【问题标题】:What made i = i++ + 1; legal in C++17?是什么让 i = i++ + 1;在 C++17 中合法吗?
【发布时间】:2018-05-21 23:35:30
【问题描述】:

在你开始大喊未定义的行为之前,这是在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-expression 是一个左值。

【问题讨论】:

  • 您已经确定了原因:在 C++17 中,右操作数在左操作数之前排序。在 C++11 中没有这样的顺序。你的问题究竟是什么?
  • @Robᵩ 见最后一句话。
  • 有没有人知道这个改变的动机?我希望静态分析器在面对i = i++ + 1; 之类的代码时能够说“你不想那样做”。
  • @NeilButterworth,来自论文p0145r3.pdf:“为惯用 C++ 优化表达式评估顺序”。
  • @NeilButterworth,第 2 节说这是违反直觉的,即使专家也无法在所有情况下做正确的事情。这几乎就是他们的全部动力。

标签: c++ language-lawyer c++17


【解决方案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 的 cmets。)

【讨论】:

  • 在该摘录中的“左操作数”所涵盖的效果中包含“实际的赋值行为”真的正确吗?该标准对实际分配的顺序有单独的语言。我认为您提供的摘录范围仅限于左手和右手子表达式的排序,结合该部分的其余部分,这似乎不足以支持良好 - OP 声明的定义性。
  • 更正:实际赋值仍然在左操作数的值计算之后排序,左操作数的评估在右操作数的(完成)评估之后排序,所以是的,这个改变就足够了支持OP询问的明确定义。那么,我只是在争论细节,但这些确实很重要,因为它们可能对不同的代码有不同的含义。
  • 我不反对,@supercat,但标准说明了它所说的内容,并且符合标准的程序必须遵守。然而,这是一种“好像”的意思:一个程序有很大的内部余地,只要它的外部行为完全好像它严格按照标准规定运行。对于您的特定示例,问题当然是编译器是否可以证明foo() 不会修改globalVariable。除非它可以这样做,否则它需要在执行函数调用之前制作该临时副本。
  • @JohnBollinger:我觉得奇怪的是,标准的作者会做出改变,这会损害甚至直接代码生成的效率,并且在历史上是不必要的,但却不愿定义其他缺乏的行为是一个更大的问题,而且很少会对效率造成任何有意义的障碍。
  • @Kaz:对于复合赋值,在右侧之后执行左侧值评估允许将x -= y; 之类的东西处理为mov eax,[y] / sub [x],eax 而不是mov eax,[x] / neg eax / add eax,[y] / mov [x],eax。我看不出有什么可笑的。如果必须指定排序,最有效的排序可能是执行所有必要的计算以首先识别左侧对象,然后评估右侧操作数,然后是 值 i> 的左对象,但这需要有一个术语来表示解决左对象的 id'ty 的行为。
【解决方案2】:

你确定了新句子

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

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

【讨论】:

    【解决方案3】:

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

    操作数的计算是无序的。

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

    此文本在 C++11 中被简单地删除,使其有些含糊不清。是UB还是不是?这已在 C++17 中阐明,他们在其中添加:

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


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

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

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

    【讨论】:

      【解决方案4】:

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

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

      i = i++;
      

      + 1 是一个红鲱鱼,并不清楚标准为什么在他们的示例中使用它,尽管我确实记得人们在 C++11 之前的邮件列表中争论说,+ 1 可能会有所作为强制在右侧进行早期左值转换。当然,这些都不适用于 C++17(可能从未适用于任何版本的 C++)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-08-17
        • 2020-05-24
        • 2016-02-20
        • 2011-10-16
        • 1970-01-01
        • 2012-08-25
        • 1970-01-01
        相关资源
        最近更新 更多