语句`int val = (++i > ++j) ? ++i : ++j;`调用未定义的行为?

Does the statement `int val = (++i > ++j) ? ++i : ++j;` invoke undefined behavior?

给定以下程序:

#include <stdio.h>
int main(void)
{
    int i = 1, j = 2;
    int val = (++i > ++j) ? ++i : ++j;
    printf("%d\n", val); // prints 4
    return 0;
}

val 的初始化似乎隐藏了一些未定义的行为,但我没有看到对象被多次修改或在没有序列点的情况下修改和使用的任何点之间。有人可以 纠正或证实我的说法?

此代码的行为已明确定义。

条件语句中的第一个表达式保证在第二个表达式或第三个表达式之前被求值,并且只有第二个或第三个表达式中的一个会被求值。 C standard:

的第 6.5.15p4 节对此进行了描述

The first operand is evaluated; there is a sequence point between its evaluation and the evaluation of the second or third operand (whichever is evaluated). The second operand is evaluated only if the first compares unequal to 0; the third operand is evaluated only if the first compares equal to 0; the result is the value of the second or third operand (whichever is evaluated), converted to the type described below.

就你的表达而言:

int val = (++i > ++j) ? ++i : ++j;

++i > ++j 首先被评估。比较时使用了ij的增量值,所以变成了2 > 3。结果为 false,因此 ++j 被计算而 ++i 不被计算。因此 j 的(再次)增量值(即 4)然后分配给 val.

为时已晚,但也许有用。

(++i > ++j) ? ++i : ++j;

在文档ISO/IEC 9899:201xAnnex C(informative)Sequence points中我们发现有一个序列点

Between the evaluations of the first operand of the conditional ?: operator and whichever of the second and third operands is evaluated

为了明确定义行为,不得在 2 个序列点之间修改 2 次(通过副作用)同一对象。

在您的表达中,唯一可能出现的冲突是在第一个和第二个 ++i++j 之间。

在每个序列点,最后存储在对象中的值应与抽象机规定的值一致(这是您在纸上计算的结果,就像在图灵机上)。

引自5.1.2.3p3 Program execution

The presence of a sequence point between the evaluation of expressions A and B implies that every value computation and side effect associated with A is sequenced before every value computation and side effect associated with B.

当您的代码中有副作用时,它们会按不同的表达式排序。规则说,在 2 个序列点之间,您可以根据需要排列这些表达式。

例如。 i = i++。由于此表达式中涉及的 none 个运算符表示序列点,因此您可以根据需要排列作为副作用的表达式。 C 语言允许你使用任何这些序列

i = i; i = i+1;i = i+1; i=i;tmp=i; i = i+1 ; i = tmp;tmp=i; i = tmp; i = i+1; 或提供与 abstract semantics of computation 相同结果的任何内容要求解释此计算。标准 ISO9899 将 C 语言定义为抽象语义。

你的程序中可能没有UB,但是问题中: 语句 int val = (++i > ++j) ? ++i : ++j; 是否调用未定义的行为?

答案是肯定的。由于 ij 是有符号的,所以一个或两个增量操作都可能溢出,在这种情况下,所有的赌注都关闭了。

当然,在您的完整示例中不会发生这种情况,因为您已将值指定为小整数。

我打算对@Doug Currie 发表评论说有符号整数溢出是一个太牵强的花絮,尽管技术上正确 作为答案。恰恰相反!

再三考虑,我认为 Doug 的答案不仅是正确的,而且假设示例中的一个不完全微不足道的三行代码(但是一个可能带有循环等的程序)应该扩展为一个清晰的,确定 "yes"。原因如下:

编译器看到 int i = 1, j = 2;,因此它 知道 ++i 将等于 j,因此不可能大于 j 甚至 ++j。现代优化器看到了如此微不足道的事情。

当然,除非其中一个溢出。但是优化器知道这将是 UB,因此假设并根据 它永远不会发生 .

进行优化

因此三元运算符的条件始终为假(在这个简单的示例中当然如此,但即使在循环中重复调用也是如此!),并且 i 只会递增 一次,而j 将始终递增两次。因此,不仅 j 总是大于 i,它甚至在每次迭代中都有收益(直到溢出发生,但根据我们的假设,这 永远不会发生 )。

因此,允许优化器无条件地将其转换为 ++i; j += 2;,这肯定不是人们所期望的。

这同样适用于例如unknown 值为 ij 的循环,例如用户提供的输入。优化器可能很好地认识到操作顺序仅取决于 ij 的初始值。因此,可以通过复制循环来优化条件移动之后的增量序列,每种情况一次,并使用单个 if(i>j) 在两者之间切换。然后,当我们这样做时,它可能会将重复递增的循环折叠成它只是添加的 (j-i)<<1 之类的东西。或者什么的。
在溢出永远不会发生的假设下——这是允许优化器做出的假设,并且确实做出——这样的修改可能会完全改变整个意义和操作模式该程序非常好。

尝试并调试它。