【问题标题】:Is (x++, y) + (y++, x) undefined or unspecified, and if unspecified, what can it compute?(x++, y) + (y++, x) 是未定义还是未指定,如果未指定,它可以计算什么?
【发布时间】:2012-12-18 15:12:11
【问题描述】:

comma sequence operator 在表达式中引入了sequence point。我想知道这是否意味着下面的程序避免了未定义的行为。

int x, y;

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

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

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

【问题讨论】:

  • 它仍然是未定义的,因为+ 的两个操作数是并行计算的。
  • @melpomene “+ 的两个操作数并行求值”的解释如何不适用于f() + g()(共识是它不是未定义的stackoverflow.com/questions/3951017/…)?跨度>
  • 我不知道如何解释。 “如果尝试修改逗号运算符的结果或在下一个序列点之后访问它,则行为未定义。”
  • @PascalCuoq 一致认为顺序是未指定的,而不是未定义的,因为在每个函数调用之前都有一个序列点,并且函数调用算作副作用,即函数执行不会相互交错。
  • 问题是,在e1 + e2 中,e1e2 的评估是未排序的还是未确定排序的?如果未排序,则为未定义的行为,如果未确定排序,则只是未指定。

标签: c c99 language-lawyer c11 unspecified-behavior


【解决方案1】:

取表达式:

(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 返回。因此,没有未定义的行为。

【讨论】:

  • 我现在明白你在评论中的意思了。谢谢你的好答案。
【解决方案2】:

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

C11 附录 J

J.1 未指定的行为

  • 子表达式的计算顺序和副作用发生的顺序,除非为 函数调用 ()、&&、||、?: 和逗号运算符 (6.5)

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

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

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

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

【讨论】:

  • 如果您的意见是y 可能会安排在y++ 旁边,那么让我们考虑表达式(0, x++, y+1) + (0, y++, x+1)。 GCC 仍然将此评估为4
  • @PascalCuoq 我不认为表达式是 UB,尽管您仍然有未指定的子表达式求值顺序。这里有 4 个子表达式,相互嵌套。该表达式等效于((0, x++), y+1) + ((0, y++), x+1),其中每个括号都是一个子表达式。我想 GCC 可能会这样评估它:0, x++ = 1, 0, y++= 1, (1, 1+1) + (1, 1+1) = 2 + 2= 4。
【解决方案3】:

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

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

但不是:

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

据我了解,唯一需要的顺序是(x++) 必须在(y) 之前,(y++) 必须在(x) 之前。

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

【讨论】:

  • 如果您的解释是编译器可以选择几种计算顺序之一,那么这就是为什么我的程序是未指定行为的解释。另一方面,未定义的行为允许编译器做任何事情(鼻恶魔),例如当程序违反 6.5:2 时(“在前一个和下一个序列点之间,对象的存储值应修改为最多一次通过表达式的评估”)。我不确定我的程序是否可以。
  • 列出的求值顺序是正确的,但是第5个是不允许的,因为运算符优先级,而不是子表达式的求值顺序。作为标准优先规则的一部分,逗号运算符本身必须始终从左到右进行计算。不,这都不能证明原始帖子中的代码是未定义的行为。
【解决方案4】:

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

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

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

【讨论】:

  • 这里的每个人,包括我在内,都同意这样做的目的是允许高效的编译。我只是在争辩说,C99 标准实际上并没有很好地说明 (0, x++, y+1) + (0, y++, x+1) 这样的表达式是未定义的。诸如 6.5:2 或 6.5:3 之类的子句没有明确说明这个表达式是未定义的。他们不使用“并行”一词,并且将函数调用和, 放在同一级别。这可能在 C11 中有所改变,但我还不熟悉。
  • @PascalCuoq,C11 在排序操作的模型上更精确一些。但是 data race 的概念只是为了线程并行性而引入的,我并不完全清楚线程概念是否相关,here。两个子表达式的求值可以被看作是两个不同的执行线程,但我不知道这是否太过分了。
猜你喜欢
  • 2011-06-22
  • 2021-10-07
  • 2012-12-14
  • 2023-01-13
  • 2019-10-08
  • 1970-01-01
  • 2015-04-09
  • 2016-01-15
  • 2017-02-21
相关资源
最近更新 更多