在您开始大喊未定义的行为之前,这明确列在 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]
除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的求值是无序的。 [...]运算符的操作数的值计算在运算符结果的值计算之前排序。如果内存位置上的副作用相对于同一内存位置上的另一个副作用或使用同一内存位置中任何对象的值的值计算是未排序的,并且它们不是潜在的并发,则行为未定义。
其中 value 在 [N4659 basic.type] 中定义
对于普通可复制类型,值表示是对象表示中确定值的一组位,该值是实现定义的一组值的一个离散元素
除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的求值是无序的。 [...]运算符的操作数的值计算在运算符结果的值计算之前排序。如果标量对象的副作用相对于同一标量对象的另一个副作用或使用同一标量对象的值的值计算是未排序的,则行为未定义。
同样,值在 [N3337 basic.type] 处定义
对于普通可复制类型,值表示是对象表示中确定值的一组位,该值是实现定义的一组值的一个离散元素。
除了提到无关紧要的并发性以及使用内存位置而不是标量对象外,它们是相同的,其中
算术类型、枚举类型、指针类型、指向成员类型的指针、std::nullptr_t 和这些类型的 cv 限定版本统称为标量类型。
这不会影响示例。
赋值运算符 (=) 和复合赋值运算符都从右到左分组。所有这些都需要一个可修改的左值作为它们的左操作数,并返回一个指向左操作数的左值。如果左操作数是位域,则所有情况下的结果都是位域。在所有情况下,赋值都在左右操作数的值计算之后和赋值表达式的值计算之前进行排序。右操作数在左操作数之前排序。
赋值运算符 (=) 和复合赋值运算符都从右到左分组。所有这些都需要一个可修改的左值作为它们的左操作数,并返回一个指向左操作数的左值。如果左操作数是位域,则所有情况下的结果都是位域。在所有情况下,赋值都在左右操作数的值计算之后和赋值表达式的值计算之前进行排序。
唯一的区别是 N3337 中没有最后一句。
然而,最后一句话不应该有任何重要性,因为左操作数 i
既不是 “另一个副作用” 也不是 “使用相同标量对象的值”因为 id-expression 是一个左值。
i = i++ + 1;
之类的代码时能够说“你不想那样做”。
在 C++11 中,“赋值”行为,即修改 LHS 的副作用,是在右操作数的值计算之后排序的。请注意,这是一个相对“弱”的保证:它只产生与 RHS 的值计算相关的排序。它没有说明 RHS 中可能存在的副作用,因为副作用的发生不是价值计算的一部分。 C++11 的要求没有在赋值行为和 RHS 的任何副作用之间建立相对顺序。这就是为 UB 创造潜力的原因。
在这种情况下,唯一的希望是 RHS 中使用的特定运算符做出的任何额外保证。如果 RHS 使用前缀 ++
,则特定于前缀形式 ++
的排序属性将在此示例中节省时间。但是后缀 ++
是另一回事:它没有做出这样的保证。在 C++11 中,=
和后缀 ++
的副作用在此示例中最终彼此无序。那就是UB。
在 C++17 中,赋值运算符的规范中增加了一个额外的语句:
右操作数在左操作数之前排序。
结合上述内容,它提供了非常强大的保证。它将 RHS 中发生的所有事情(包括任何副作用)排在 LHS 中发生的所有事情之前。由于实际分配是在 LHS(和 RHS)之后排序的,因此额外的排序将分配行为与 RHS 中存在的任何副作用完全隔离开来。这种更强的排序是消除上述 UB 的原因。
(更新以考虑@John Bollinger 的评论。)
你确定了新句子
右操作数在左操作数之前排序。
并且您正确地确定了左操作数作为左值的评估是无关紧要的。但是,sequenced before 被指定为传递关系。因此,完整的右操作数(包括后增量)也在赋值之前排序。在 C++11 中,只有右操作数的值计算在赋值之前进行排序。
在较早的 C++ 标准和 C11 中,赋值运算符文本的定义以文本结尾:
操作数的评估是无序的。
这意味着操作数中的副作用是未排序的,因此如果它们使用相同的变量,则肯定是未定义的行为。
该文本在 C++11 中被简单地删除,使其有些含糊不清。是UB还是不是?这已在 C++17 中得到澄清,他们在其中添加:
右操作数在左操作数之前排序。
作为旁注,在更旧的标准中,这一切都非常清楚,例如来自 C99:
操作数的求值顺序未指定。如果尝试修改赋值运算符的结果或在下一个序列点之后访问它,则行为未定义。
基本上,在 C11/C++11 中,他们在删除此文本时搞砸了。
这是其他答案的更多信息,我将其发布为下面的代码也经常被问到。
其他答案中的解释是正确的,也适用于现在定义明确的以下代码(并且不会更改 i
的存储值):
i = i++;
+ 1
是一个红鲱鱼,并不清楚标准为什么在他们的示例中使用它,尽管我确实记得人们在 C++11 之前的邮件列表中争论说,也许 + 1
由于提前强制而有所作为右侧的左值转换。当然,这些都不适用于 C++17(并且可能从未应用于任何版本的 C++)。
foo()
不会修改globalVariable
。除非它可以这样做,否则它需要在执行函数调用之前制作该临时副本。x -= y;
之类的东西作为mov eax,[y] / sub [x],eax
而不是mov eax,[x] / neg eax / add eax,[y] / mov [x],eax
处理。我看不出有什么可笑的。如果必须指定排序,最有效的排序可能是执行所有必要的计算以首先识别左侧对象,然后评估右侧操作数,然后是 值 i> 的左对象,但这需要有一个术语来表示解决左对象的 id'ty 的行为。