是什么使i = i ++ + 1; 合法的C++ 17?

Pas*_* By 179 c++ language-lawyer c++17

在开始大喊未定义的行为之前,这在N4659(C++ 17)中明确列出

  i = i++ + 1;        // the value of i is incremented
Run Code Online (Sandbox Code Playgroud)

然而在N3337(C++ 11)

  i = i++ + 1;        // the behavior is undefined
Run Code Online (Sandbox Code Playgroud)

改变了什么?

从我可以收集,从[N4659 basic.exec]

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

其中定义为[N4659 basic.type]

对于简单的可复制类型,值表示是对象表示中的一组位,用于确定,该是实现定义的值集的一个离散元素

来自[N3337 basic.exec]

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

同样,值定义在[N3337 basic.type]

对于简单的可复制类型,值表示是对象表示中的一组位,用于确定,该是实现定义的值集的一个离散元素.

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

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

这不会影响示例.

来自[N4659 expr.ass]

赋值运算符(=)和复合赋值运算符都是从右到左分组.所有都需要一个可修改的左值作为左操作数,并返回一个左值操作数的左值.如果左操作数是位字段,则所有情况下的结果都是位字段.在所有情况下,在右和左操作数的值计算之后,以及在赋值表达式的值计算之前,对赋值进行排序.右操作数在左操作数之前排序.

来自[N3337 expr.ass]

赋值运算符(=)和复合赋值运算符都是从右到左分组.所有都需要一个可修改的左值作为左操作数,并返回一个左值操作数的左值.如果左操作数是位字段,则所有情况下的结果都是位字段.在所有情况下,在右和左操作数的值计算之后,以及在赋值表达式的值计算之前,对赋值进行排序.

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

然而,最后一句话不应该具有任何重要性,因为左操作数i既不是"另一个副作用"也不是"使用相同标量对象的值",因为id-expression是左值.

AnT*_*AnT 139

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

在这种情况下,唯一的希望是RHS中使用的特定操作员提供的任何额外保证.如果RHS使用了前缀++,那么特定于前缀形式的排序属性++将在此示例中保存一天.但后缀++是一个不同的故事:它没有做出这样的保证.在C++ 11中,在这个例子中,=和后缀的副作用++最终没有相互关联.那就是UB.

在C++ 17中,在赋值运算符的规范中添加了一个额外的句子:

右操作数在左操作数之前排序.

与上述相结合,它提供了非常有力的保证.它序列一切之前发生在RHS(包括任何副作用)一切这种情况发生在LHS.由于实际分配 LHS(和RHS)之后进行测序,因此额外测序完全将分配行为与RHS中存在的任何副作用隔离开来.这种更强的测序是消除上述UB的原因.

(更新以考虑@John Bollinger的评论.)

  • 校正:在左操作数的值计算之后仍然对实际赋值进行排序,并且在对右操作数进行(完全)求值之后对左操作数的求值进行排序,所以是的,该变化足以支持OP的明确定义问道.我只是在解决细节,但这些确实很重要,因为它们可能对不同的代码有不同的含义. (10认同)
  • 在摘录中"左手操作数"所涵盖的效果中加入"实际的转让行为"是否真的是正确的?该标准有关于实际分配顺序的单独语言.我把你所提到的摘录限制在左手和右手子表达式的排序范围内,这似乎不足以与该部分的其余部分结合起来,以支持OP声明的定义. (3认同)
  • @JohnBollinger:我很遗憾标准的作者会做出改变,这会影响甚至直接代码生成的效率,并且在历史上并不是必要的,但却不愿意定义其他行为,这些行为的缺失是一个更大的问题,哪些很少会对效率造成任何有意义的障碍. (2认同)

小智 33

你确定了新句子

右操作数在左操作数之前排序.

并且您正确地确定左操作数作为左值的评估是无关紧要的.但是,之前的顺序被指定为传递关系.因此,完整的右操作数(包括后增量)也在赋值之前排序.在C++ 11中,只有右操作数的值计算在赋值之前被排序.


Lun*_*din 7

在旧的C++标准和C11中,赋值运算符文本的定义以文本结尾:

对操作数的评估是不确定的.

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

在C++ 11中简单地删除了这个文本,使它有点含糊不清.它是UB还是不是?这已在C++ 17中得到澄清,他们补充说:

右操作数在左操作数之前排序.


作为旁注,在更老的标准中,这一切都非常明确,例如来自C99:

操作数的评估顺序未指定.如果尝试修改赋值运算符的结果或在下一个序列点之后访问它,则行为未定义.

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