代码之家  ›  专栏  ›  技术社区  ›  pablo1977

函数调用中的格式良好的配对

  •  2
  • pablo1977  · 技术社区  · 8 年前

    这是关于标准C11中的规范的问题,涉及在表达式中计算函数参数时的副作用。

    我试图用标准C定义一个宏,它以一种基本的方式模拟OOP语言的类似“方法”的语法。
    我已经设计了一个解决方案,我将在这里介绍其主要思想,但我对其是否符合C11有些怀疑。
    我需要先做说明,最后我将提出具体问题,这与涉及函数调用的表达式的求值有关。 很抱歉,邮件太长。

    所以,给定一个 struct ,或well a struct * 对象 x

    x->foo_method();  
    

    解决此问题的典型方式如下:

    • 通过 结构 宣言:

      typedef struct foo_s { void foo_method(struct foo_s * this); } *foo_class;  
      foo_class x   = malloc(sizeof(struct foo_s));  
      x->foo_method = some_function_defined_over_there___;  
      
    • 然后通过重复“this”参数中的对象进行调用:

      x->foo_method(x);  
      

    可以尝试定义某种“方法调用”宏:

    #define call(X, M) ((X)->M(X))
    

    然而,这种方法是不好的,因为 X 可以复制副作用(这是众所周知的重复宏参数两次的缺陷)。

    [通过使用巧妙的宏,我可以处理任意数量的方法参数的情况 M 例如,通过使用 __VA_ARGS__ 以及一些中间宏黑客。]

    为了解决宏参数重复的问题,我决定实现一个全局堆栈,可能在函数中隐藏为静态数组:

    (void*) my_stack(void* x, char* operation)
    {
        static void* stack[100] = { NULL, }; 
        // 'operation' selects "push" or "pop" operations on the stack.
        // ...
        // IF (operation == "push") then 'x' itself is returned again.
    }
    

    因此,我现在只写一次“X”,以避免在宏中重复出现副作用:

    #define call(X, M) (((foo_class)my_stack((X), "push")) -> M (my_stack(0,"pop")))
    

    如您所见,我的意图是C编译器将类似函数的宏视为表达式,其值是方法返回的值 M .
    我只写过一次情妇 十、 在宏体内,其值存储在堆栈中。由于需要此值,因此可以访问 十、 这就是为什么函数 my_stack 返回的值 :我需要立即将其重用为推送值的同一表达式的一部分 x个 在堆栈中。


    十、 call(X,M) 宏观上,它们会出现更多问题。

    • 通过使用相同的宏,可以拥有其参数也是存储在堆栈中的对象的“方法” call() .
    • 更重要的是,我们可以在“方法”中有参数,这些参数的值是通过评估其他“方法”而获得的。
    • 最后,作为给定“方法”的参数出现的其他函数或方法可能会修改堆栈,因为它们很可能是通过使用 调用()

    我希望我的宏在所有这些情况下都保持一致。例如,让我们假设 x1,x2,x3 foo_class 物体。
    另一方面,让我们假设 foo_class类 ,以下“方法”成员:

     int (*meth)(foo_class this, int, int);
    

    最后,我们可以进行“方法”调用:

     call(x1, meth, (call (x2, 2, 2), call(x3, 3, 3)) ) ;
    

    [宏的真正语法没有必要像上面显示的那样。我希望能够理解其主要思想。]

    x1->meth(x1, x2->meth(x2,2,2), x3->meth(x3,3,3));  
    

    x1->meth(x1,....) , x2->meth(x2,...) , x3->meth(x3,...) .

    例如: ((foo_class)(my_stack(x2,"push"))) -> meth (my_stack(0,"pop"), ...) .

    我的问题是: 我能不能确保在任何可能的表达式中都使用“push”/“pop”(始终使用 调用() 宏)始终提供预期的对象对?

    例如,如果我正在“推”x2,那么x3被“弹出”是完全错误的。

    我的猜测是: 答案是肯定的,但在围绕以下主题对标准文件ISO C11进行深入分析之后 .

    • 因此,例如, x1 存储在堆栈中 meth 方法被视为被调用。
    • 在传递给函数的所有参数的求值之后和实际函数调用之前都有一个序列点。
      因此,例如,如果新对象 x4, x5 调用 x1->meth(x1...x2...x3) 发生,这些对象 x4 x5 将在堆栈中出现和消失 x2, x3 ,已从堆栈中删除。
    • 函数调用中的参数之间没有任何序列点。
      因此,在计算以下表达式时(当它们是上面显示的函数调用的参数时,涉及 x1、x2、x3 ):

      my_stack(x2,"push") -> meth(my_stack(0,"pop"),2,2) 
      my_stack(x3,"push") -> meth(my_stack(0,"pop"),3,3) 
      

      可能发生在 x2 x3 如果“推送”到堆栈中,“弹出”操作可能会出现配对错误: x3个 可以在 meth(...,2,2) 行,和 2倍 可以在 meth(...,3,3) 线,相对于所需的。

      这种情况完全不可能发生,而且在标准C99下似乎没有正式的解决方案。

    然而,在C11中,我们有以下概念 inderminately sequenced 副作用。
    我们有:

    • 当调用一个函数时,它的所有副作用都会按照围绕该函数调用的表达式的任何其他表达式的不确定顺序来解决。[见下文第(12)段: sequence points ].

    • 由于函数调用中的副作用 冰毒

       my_stack(x2,"push") -> meth(my_stack(0,"pop"),2,2)
      

      必须“在”或“完全在”副作用之前或之后解决:

       my_stack(x3,"push") -> meth(my_stack(0,"pop"),3,3) 
      

      我的结论是“推”和“弹出”操作是很好的组合 .

    我对标准的解释可以吗? 我将引用它,以防万一:

    [C11,6.5.2.2/10] 在函数指示符和实际参数的求值之后,但在实际调用之前,有一个序列点。调用函数(包括函数调用)中的每个求值,如果在被调用函数体执行之前或之后没有以其他方式明确排序,则会针对被调用函数的执行进行不确定的排序。94)
    [脚注94] :换句话说,函数执行不会相互交错。

    也就是说,虽然无法预测函数调用中参数的求值顺序,但我认为无论如何,可以确保ISO C11中建立的“顺序”规则足以确保“推”和“弹出”操作在这种情况下正常工作。

    因此,在C语言中可以使用类似“方法”的语法,以模拟“方法作为对象的成员”的基本但一致的OOP功能。

    1 回复  |  直到 8 年前
        1
  •  2
  •   Jens Gustedt    8 年前

    不,我认为你不能保证这是你想要的。让我们分解您的表达式

    my_stack(x2,"push") -> meth(my_stack(0,"pop"),2,2)
    <<<<<< A >>>>>>>>>>         <<<<<<< B >>>>>>
    <<<<<<<<<<<<< C >>>>>>>>>>>
    <<<<<<<<<<<<<<<<<<<<<<< D >>>>>>>>>>>>>>>>>>>>>>>>
    

    B和C的求值是完全独立的,必须在函数调用D之前完成。函数的参数和函数指示符没有太大区别。

    因为A和B是函数调用,它们实际上是按顺序排列的,但这是不确定的,所以您不知道哪个先来,哪个后来。

    我想如果你能 call 内联函数。如果您真的需要 呼叫 对于不同的类型,可以使用 _Generic 表示但正如有人在评论中所说,你真的处于在C语言中应该合理做的事情的极限。