是什么使i = i ++ +1; 在C ++ 17中合法吗?


186

在开始大叫未定义的行为之前,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 basic.type]

对于平凡可复制的类型,值表示形式是对象表示形式中确定值的一组位,该是实现定义的一组值中的一个离散元素

[N3337 basic.exec]

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

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

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

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

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

这不影响示例。

来自[N4659 expr.ass]

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

来自[N3337 expr.ass]

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

唯一的不同是N3337中没有最后一个句子。

但是,最后一句话应该没有任何意义,因为左操作数i既不是“另一个副作用”,也不是“使用相同标量对象的值”,因为id表达式是一个左值。


23
您已找到原因:在C ++ 17中,右操作数在左操作数之前排序。在C ++ 11中,没有这样的排序。确切地说,您的问题是什么?
罗伯·

4
@Robᵩ参见最后一句话。
过客赛

7
有没有人链接到此更改的动机?我希望静态分析器在面对诸如此类的代码时能够说“您不想这样做” i = i++ + 1;

7
@NeilButterworth,来自论文p0145r3.pdf:“为惯用C ++定义表达式评估顺序”。
xaizek

9
@NeilButterworth,第2节说这是反直观的,甚至专家在所有情况下都做不到正确的事情。这几乎就是他们的动力。
xaizek

Answers:


144

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

在这种情况下,唯一的希望是RHS中使用的特定运营商提供的任何其他保证。如果RHS使用prefix ++++则在该示例中,特定于前缀形式的序列化属性将节省大量时间。但是后缀++是另外一回事:它没有做出这样的保证。在此示例中,在C ++ 11中,=和postfix 的副作用++最终是彼此无关的。那就是UB。

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

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

结合以上所述,可以提供非常有力的保证。它先于RHS中发生的一切(包括任何副作用)进行排序,然后再将LHS中发生的一切排序。由于实际分配是 LHS(和RHS)之后进行排序的,因此额外的排序将分配行为与RHS中存在的任何副作用完全隔离开来。这种更强大的排序消除了上述UB。

(已更新,以考虑到@John Bollinger的评论。)


3
在该摘录的“左操作数”所涵盖的效果中包含“实际分配行为”真的正确吗?该标准具有关于实际分配顺序的单独语言。我摘录的摘录内容仅限于左右两个子表达式的排序,结合本节的其余部分来看,这似乎不足以支持- OP语句的定义。
John Bollinger

11
更正:实际赋值仍在左操作数的值计算之后进行,而左操作数的评估在对右操作数进行(完整)评估后进行排序,因此,是的,这一更改足以支持OP的明确定义问了一下。然后,我只是在争辩细节,但这很重要,因为它们可能对不同的代码产生不同的影响。
John Bollinger

3
@JohnBollinger:我很好奇该标准的作者会做出改变,从而损害甚至直接生成代码的效率,并且在历史上是不必要的,但是却不愿定义其他行为,而缺少行为则是一个更大的问题,并且很少会对效率产生任何有意义的障碍。
超级猫

1
@Kaz:对于复合赋值,在右侧之后执行左侧值评估x -= y;可以将mov eax,[y] / sub [x],eax而不是进行类似处理mov eax,[x] / neg eax / add eax,[y] / mov [x],eax。我看不出有什么特别的。如果必须指定一种排序,则最有效的排序可能是先执行识别左侧对象所需的所有计算,然后评估右侧操作数,然后评估左侧对象的,但这需要有一个条件解决左对象的id'ty的行为。
超级猫

1
@Kaz:如果xyvolatile,那会产生副作用。此外,同样的考虑也适用于x += f();,在那里f()修改x
超级猫

33

您确定了新句子

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

并且您正确地确定了左操作数作为左值的评估是不相关的。但是,之前排序被指定为传递关系。因此,完整的权限操作数(包括后递增)将在赋值之前进行排序。在C ++ 11中,赋值之前仅对右操作数的值计算进行排序。


7

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

操作数的评估是无序列的。

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

这段文字在C ++ 11中被简单地删除,因此有些模棱两可。是UB还是不是?他们在C ++ 17中对此进行了澄清:

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


附带说明一下,在更旧的标准中,这一切都非常清楚,例如C99:

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

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


1

这是其他答案的进一步信息,我也将其发布,因为下面的代码也经常被问到

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

i = i++;

+ 1是一条红色的鲱鱼,虽然我确实记得有人在C ++ 11之前的邮件列表上争论,也许是+ 1由于强制在右边进行早期左值转换而有所作为,但尚不清楚标准为何在示例中使用它。手侧。当然,这些都不适用于C ++ 17(并且可能从未适用于任何版本的C ++)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.