函数调用中格式正确的配对

Well formed pairings in function call

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

我正在尝试在标准 C 中定义一个宏,以基本方式模拟 OOP 语言的 "method" 类语法。
我设计了一个解决方案,我将在这里公开其主要思想,但我对它是否符合 C11 有一些疑问。
我需要先做说明,最后我会提出具体问题,这与涉及函数调用的表达式的评估有关。 抱歉长post.

所以,给定一个 struct,或者一个 struct * 对象 x,如果我可以这样调用 "method",我会很高兴:

x->foo_method();  

解决这个问题的典型方式是这样的:

可以尝试定义某种 "method-call" 宏:

#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.
返回的值 我只在宏体内写了一次参数 X ,它的值存储在堆栈中。由于需要此值能够访问 X 本身的 "method" 成员,这就是函数 my_stack returns 的值 x 的原因:我需要立即重用它作为将值 x 推入堆栈的同一表达式的一部分。


虽然这个思路似乎很容易解决call(X,M)宏中X重复的问题,但它们出现的问题更多。

我希望我的宏在所有情况下都保持一致。例如,假设 x1,x2,x3foo_class 个对象。
另一方面,假设我们在 foo_class 中有以下 "method" 成员:

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

最后,我们可以进行 "method" 调用:

 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"(始终使用 call() macro) 总是给出预期的一对对象?

例如,如果我是 "pushing" x2,那么 x3 是 "popped" 就完全错误了。

我的猜想是: 答案是肯定的,但在围绕 序列点 [=160] 主题深入分析标准文档 ISO C11 之后=].

  • 在产生要调用的"method"(实际上是"function")的表达式和要传递给它的参数的表达式之间有一个序列点。 因此,例如,x1meth 方法被认为被调用之前存储在堆栈中。
  • 在计算传递给函数的所有参数之后和实际函数调用之前有一个序列点。
    因此,例如,如果在调用 x1->meth(x1...x2...x3) 时新对象 x4, x5 等在堆栈中 "pushed"/"popped",则这些对象 x4x5 会在 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) 
    

    可能发生在对象 x2x3 在堆栈中成为 "pushed" 之后,"pop" 操作可能会发生错误配对:x3可能是 meth(...,2,2) 行中的 "popped",而 x2 可能是 meth(...,3,3) 行中的 "popped",这与期望的相反。

    这种情况g是完全不可能的,在Standard C99下好像没有正式的解决办法。

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

  • 当一个函数被调用时,它的所有副作用都会根据调用函数的表达式周围的任何其他表达式以不确定的顺序解决。 [参见此处的第 (12) 段:sequence points]。

  • 由于meth函数调用中的副作用涉及表达式:

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

    必须解决 "completely before" 或 "completely after" 中的副作用:

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

    我得出结论,"push" 和 "pop" 操作配对得很好

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

[C11, 6.5.2.2/10] There is a sequence point after the evaluations of the function designator and the actual arguments but before the actual call. Every evaluation in the calling function (including function calls) that is not otherwise specifically sequenced before or after the execution of the body of the called function is indeterminately sequenced with respect to the execution of the called function.94)
[Footnote 94]: In other words, function executions do not ‘‘interleave’’ with each other.

也就是说,虽然无法预测函数调用中参数的求值顺序,但我认为无论如何可以确定 ISO C11 中建立的 "sequence" 规则足以确保"push" 和 "pop" 操作在这种情况下运行良好。

因此,可以在 C 中使用类似于 "method" 的语法,以模拟 "methods as members of objects".

的基本但一致的 OOP 功能

不,我不认为你能保证这会做你want.Let我们分解你的表达

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

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

因为A和B是函数调用,其实是有先后顺序的,但这是不确定的,不知道哪个先到哪个后。

我认为将 call 设为内联函数会更好。如果您确实需要不同类型的 call 版本,您可以使用 _Generic 表达式转到 select 函数。但是正如有人在评论中所说的那样,您确实处于 C 中应该合理执行的操作的极限。