函数调用中参数的评估顺序?

我正在研究C中的未定义行为,并且我发表了一份声明

函数参数的评估没有特定的顺序

但那么像_cdecl_stdcall这样的标准调用约定呢,其定义(在一本书中)表明参数是从右到左进行评估的。

现在我对这两个定义感到困惑,根据UB,状态与另一个定义不同,这是根据调用约定的定义。 请certificate两者合理。

正如Graznarak的答案正确指出的那样, 评估参数的顺序与参数传递的顺序不同。

ABI通常仅适用于传递参数的顺序,例如使用哪些寄存器和/或将参数值推入堆栈的顺序。

C标准所说的是评估顺序未指定。 例如(记住printf返回一个int结果):

 some_func(printf("first\n"), printf("second\n")); 

C标准说两条消息将按某种顺序打印(评估不是交错的),但明确没有说明选择了哪个顺序。 它甚至可以在不同的呼叫之间变化,而不违反C标准。 它甚至可以评估第一个参数,然后计算第二个参数,然后将第二个参数的结果推送到堆栈,然后将第一个参数的结果推送到堆栈。

ABI可以指定哪些寄存器用于传递两个参数,或者指定堆栈中推送值的确切位置,这完全符合C标准的要求。

但即使ABI实际上要求评估以指定的顺序发生(例如,打印"second\n"后跟"first\n"将违反ABI),这仍然与C标准一致。

C标准所说的是C标准本身并没有定义评估顺序。 一些二级标准仍然可以免费使用。

顺便说一下,这本身并不涉及未定义的行为。 在某些情况下,未指定的评估顺序可能导致未定义的行为,例如:

 printf("%d %d\n", i++, i++); /* undefined behavior! */ 

_cdecl_stdcall只是指定以从右到左的顺序将参数压入堆栈 ,而不是按顺序计算它们。 想想如果调用_cdecl_stdcallpascal等约定改变了参数被评估的顺序会发生什么。

如果通过调用约定修改了评估顺序,则必须知道要调用的函数的调用约定,以便了解自己的代码的行为方式。 如果我见过一个,这是一个漏洞的抽象。 埋藏在别人写的头文件中的某个地方,对于理解这一行代码来说,将是一个神秘的关键; 但是你有几十万行,并且每个行为都会发生变化? 那将是精神错乱。

我觉得C89中的许多未定义行为源于标准是在存在多个冲突实现之后编写的。 他们可能更关心的是同意一个理智的基线,大多数实施者都可以接受,而不是定义所有行为。 我喜欢认为C中所有未定义的行为只是一群聪明而充满激情的人同意不同意的地方,但我不在那里。

我现在想要分叉一个C编译器并让它评估函数参数,好像它们是一个二叉树,我正在运行广度优先遍历。 对于未定义的行为,你永远不会有太多的乐趣!

论证评估和论证传递是相关但不同的问题。

参数往往是从左到右传递的,通常是在寄存器而不是堆栈中传递一些参数。 这是ABI和_cdecl_stdcall指定的内容。

在将参数放置在函数调用所需的位置之前,对参数的评估顺序是未指定的。 它可以从左到右,从右到左或其他顺序评估它们。 这取决于编译器,甚至可能根据优化级别而有所不同。

检查你提到的关于“序列点”的任何参考书,因为我认为这是你想要的。
基本上,序列点是一个点,一旦你到达那里,你就可以确定所有前面的表达式都已被完全评估,并且它的副作用肯定不会再存在。

例如,初始化程序的结尾是序列点。 这意味着之后:

 bool foo = !(i++ > j); 

你确定i将等于i的初始值+1,并且foo被指定为truefalse 。 另一个例子:

 int bar = i++ > j ? i : j; 

完全可以预测。 它如下所示:如果i当前值大于j ,并且在此比较之后将i加1(问号是序列点,那么在比较之后, i递增),则分配i (新值)到bar ,否则分配j 。 这可以归结为三元运算符中的问号也是有效的序列点。

C99标准(附件C)中列出的所有序列点均为:

以下是5.1.2.3中描述的序列点:
– 在评估参数后调用函数(6.5.2.2)。
– 以下运算符的第一个操作数的结尾:logical AND &&(6.5.13); 逻辑OR || (6.5.14); 条件? (6.5.15); 逗号,(6.5.17)。
– 完整声明者的结尾:声明者(6.7.5);
– 完整表达式的结束:初始化器(6.7.8); 表达式中的表达式(6.8.3); 选择语句的控制表达式(if或switch)(6.8.4); while或do语句的控制表达式(6.8.5); for语句的每个表达式(6.8.5.3); 返回语句中的表达式(6.8.6.4)。
– 在库函数返回之前(7.1.4)。
– 与每个格式化输入/输出函数转换说明符(7.19.6,7.24.2)相关联的操作之后。
– 在每次调用比较函数之前和之后,以及对比较函数的任何调用和作为参数传递给对象的任何移动之间(7.20.5)。

这意味着,实质上是任何未跟随序列点的表达式都可以调用未定义的行为,例如:

 printf("%d, %d and %d\n", i++, i++, i--); 

在此语句中,适用的序列点是“在评估参数后调用函数” 。 在评估参数之后。 如果我们再看看语义,在6.5.2.2,第10点的相同标准中,我们看到:

10函数指示符的评估顺序,实际参数和实际参数中的子表达式是未指定的,但在实际调用之前有一个序列点。

这意味着对于i = 1 ,传递给printf的值可以是:

 1, 2, 3//left to right 

但同样有效的是:

 1, 0, 1//evaluated i-- first //or 1, 2, 1//evaluated i-- second 

可以肯定的是,此次通话后i的新值将为2。
但是,从理论上讲,上面列出的所有值都同样有效,并且符合100%标准。

但是关于未定义行为的附录明确地将其列为调用未定义行为的代码:

在两个序列点之间,对象被多次修改,或者被修改并且读取先前值而不是确定要存储的值(6.5)。

理论上,你的程序可能崩溃,而不是printinf 1, 2, and 3 ,输出"666, 666 and 666" 666,666 "666, 666 and 666"也是可能的

所以最后我发现了……是的。 这是因为参数在它们被评估之后被传递。所以传递参数是一个完全不同于评估的故事.c的编译器因为它传统上是为了最大化速度而构建,并且优化可以以任何方式评估表达式。
因此,论证传递和评价都是不同的故事。

由于C标准没有指定任何评估参数的顺序,因此每个编译器实现都可以自由采用一个。 这就是为什么像foo(i++)这样的编码是完全疯狂的一个原因 – 在切换编译器时可能会得到不同的结果。

另一个重要的事情是这里没有突出显示 – 如果你最喜欢的ARM编译器从左到右评估参数,它将对所有情况和所有后续版本进行评估。 读取编译器参数的顺序仅仅是一种惯例……