是什么让 i = i++ + 1?这在 C++17 中合法吗?

在你开始对未定义的行为大喊大叫之前,这是在 N4659(C++17)中显示列出的

  i = i++ + 1;        // the value of i is incremented

但在N3337 (C++11)

  i = i++ + 1;        // the behavior is undefined

什么变了?

据我所知,从[N4659 Basic.Exec]

除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的求值是未排序的。[..]运算符的操作数的值计算在运算符结果的值计算之前排序。如果内存位置上的副作用相对于同一内存位置上的另一个副作用或使用同一内存位置中的任何对象的值进行的值计算是无序的,并且它们不是潜在并发的,则该行为是未定义的。

其中,价值[N4659基本类型]处定义

对于普通可复制类型,值表示是对象表示中确定价值的一组位,它是实现定义的一组值中的一个离散元素

[N3337基本.执行]

除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的求值是未排序的。[..]运算符的操作数的值计算在运算符结果的值计算之前排序。如果标量对象上的副作用相对于同一标量对象上的另一副作用或使用同一标量对象值的值计算是未排序的,则行为是未定义的。

同样,在[N3337基本类型]处定义值

对于普通可复制类型,值表示是对象表示中的一组位,用于确定价值,这是实现定义的一组值中的一个离散元素。

它们是相同的,只是提到了并发性(这无关紧要),并且使用了内存位置而不是标量对象,其中

算术类型、枚举类型、指针类型、指向成员类型的指针、std::nullptr_t以及这些类型的cv限定版本统称为标量类型。

这并不影响示例。

[N4659 EXPR.ASS]

赋值运算符(=)和复合赋值运算符都从右向左分组。它们都需要一个可修改的左值作为左操作数,并返回一个引用左操作数的左值。如果左操作数是位域,则所有情况下的结果都是位域。在所有情况下,赋值都在右操作数和左操作数的值计算之后、赋值表达式的值计算之前排序。右操作数的顺序在左操作数之前。

[N3337 EXPR.ASS]

赋值运算符(=)和复合赋值运算符都从右向左分组。它们都需要一个可修改的左值作为左操作数,并返回一个引用左操作数的左值。如果左操作数是位域,则所有情况下的结果都是位域。在所有情况下,赋值都在右操作数和左操作数的值计算之后、赋值表达式的值计算之前排序。

唯一的区别是N3337中没有最后一句话。

然而,最后一句不应该有任何重要性,因为左操作数i既不是“另一个副作用”,也不是“使用同一标量对象的值”,因为ID-表达式是左值。

15768 次浏览

在C++11中,“赋值”动作,即修改LHS的副作用,在右操作数的值计算之后排序。请注意,这是一个相对“弱”的保证:它仅产生与RHS的值计算相关的序列。它没有说明RHS中可能存在的副作用,因为副作用的发生不是值计算的一部分。C++11的要求没有在赋值行为和RHS的任何副作用之间建立相对排序。这就是为UB创造潜力的原因。

在这种情况下,唯一的希望是RHS中使用的特定运营商提供的任何额外保证。如果RHS使用前缀++,则特定于++的前缀形式的排序属性将在此示例中节省时间。但后缀++则是另一回事:它没有做出这样的保证。在C++11中,=和后缀++的副作用在本例中以彼此不相关的顺序结束。那就是乌布。

在C++17中,一个额外的句子被添加到赋值操作符的规范中:

右操作数的顺序在左操作数之前。

结合上述内容,这是一个非常有力的保证。它将RHS中发生的每件事(包括任何副作用)排在LHS中发生的每件事之前。由于实际分配是之后 LHS(和RHS)进行测序的,因此额外的测序将分配行为与RHS中存在的任何副作用完全隔离。这种更强的测序消除了上述UB.

(更新时考虑了@John Bollinger的评论。)

你确定了新的句子。

右操作数的顺序在左操作数之前。

并且您正确地确定了左操作数作为左值的计算是不相关的。然而,之前已排序被指定为传递关系。因此,完整的右操作数(包括后增量)是在赋值之前排序的。在C++11中,只有右操作数的值计算在赋值前排序。

在较早的C++标准和C11中,赋值运算符文本的定义以以下文本结束:

操作数的求值是未排序的。

这意味着操作数中的副作用是未排序的,因此如果它们使用相同的变量,则肯定是未定义的行为。

这个文本在C++11中被简单地删除了,留下了一些模棱两可的内容。是UB还是不是?这一点在C++17中得到了澄清,他们补充道:

右操作数的顺序在左操作数之前。


作为旁注,在更老的标准中,这一切都非常清楚,例如C99:

未指定操作数的求值

顺序。如果试图修改 赋值运算符的结果,或者要在下一个序列点之后访问它,则 行为未定义.

基本上,在C11/C++11中,当他们删除这个文本时,他们搞砸了。

这是其他答案的进一步信息,我把它贴出来,因为下面的代码也经常被问到。

其他答案中的解释是正确的,并且也适用于现在定义良好的以下代码(并且不会更改i的存储值):

i = i++;

+ 1是一个障眼法,不太清楚为什么标准在其示例中使用它,尽管我确实记得人们在C++11之前的邮件列表中争论说,+ 1可能由于在右侧强制进行早期左值转换而产生差异。当然,这些都不适用于C++17(而且可能从未适用于C++的任何版本)。