1

逗号序列运算符在表达式中引入序列点。我想知道这是否意味着下面的程序避免了未定义的行为。

int x, y;

int main()
{
  return (x++, y) + (y++, x);
}

如果它确实避免了未定义的行为,它仍然可以是未指定的,也就是说,返回几个可能的值之一。我认为在 C99 中,它只能计算1,但实际上,各种版本的 GCC 将该程序编译为返回的可执行文件2。Clang 生成一个返回的可执行文件1,显然符合我的直觉。

最后,这在 C11 中是否发生了变化?

4

4 回答 4

6

取表达式:

(x++, y) + (y++, x)

从左到右评估:

x++  // yield 0, schedule increment of x
,    // sequence point: x definitely incremented now
y    // yield 0
y++  // yield 0, schedule increment of y
// explode because you just read from y and wrote to y
// with no intervening sequence point

标准中没有任何内容禁止这样做,所以整个事情都有未定义的行为。

对比这个伪代码:

f() { return x++, y; }
g() { return y++, x; }
f() + g()

根据 C99 (5.1.2.3/2),对fg本身的调用都算作副作用,并且函数调用运算符在进入函数之前包含一个序列点。这意味着函数执行不能交错。

在“并行评估事物”模型下:

f()  // arbitrarily start with f: sequence point; enter f
g()  // at the same time, start calling g: sequence point

由于执行f计数本身就是副作用,因此序列点会g()暂停执行,直到f返回。因此,没有未定义的行为。

于 2012-12-18T15:59:00.003 回答
5

该标准的整个第 6.5 章都提到了基于操作员的评估顺序。我能找到的评估顺序的最佳总结是标准的(非规范性)附录 J:

C11 附录 J

J.1 未指明的行为

  • 评估子表达式的顺序和发生副作用的顺序,函数调用 ()、&&、||、?: 和逗号运算符 (6.5) 中指定的除外

在您的示例中,您无法知道是否首先评估子表达式(x++, y)or (y++, x),因为未指定 + 运算符的操作数的评估顺序。

至于未定义的行为,逗号运算符什么也没解决。如果(x++, y)首先评估,则y可能会y++在另一个子表达式之前立即评估。由于 y 被访问两次,中间没有序列点,因此除了确定要存储的值之外,行为是未定义的。更多信息在这里。

所以你的程序都有未定义和未指定的行为。

(此外,它具有实现定义的行为,因为 int main() 而不是 int main (void) 不是托管应用程序中明确定义的 main 版本之一。)

于 2012-12-18T16:13:05.250 回答
1

我的理解是,以下任何评估命令都是合法的:

  • x++, y++, y, x
  • x++, y, y++, x
  • x++, y++, x, y
  • y++, x, x++, y
  • ...

但不是:

  • y, x++, x, y++
  • ...

在我的理解中,唯一需要的顺序是(x++)must come before (y),并且(y++)must come before (x)

因此,我认为这仍然是未定义的行为。

于 2012-12-18T15:26:46.740 回答
1

该标准明确声明了哪些运算符引入了序列点,+而不是其中。因此,您的表达式可以很好地被评估为被禁止的子表达式被评估为“接近”彼此,它们之间没有序列点。

在实用的基础上,这种禁止是有原因的,即这种操作的操作数的评估可以并行调度,例如,如果处理器具有用于所讨论的子表达式的操作的多个流水线。你可以很容易地说服自己,在这样的评估模式下,任何事情都有可能发生,结果是不可预测的。

在您的玩具示例中,冲突是可预测的,但如果您在子表达式中使用一些指针间接,则不一定是这种情况。那么您将不知道其中一个子表达式是否可能会为另一个子表达式设置别名。因此,如果 C 想要允许这种类型的优化,基本上没有什么可以做的。

于 2012-12-18T16:54:45.573 回答