评估顺序和未定义的行为

Kol*_*nya 18 c++ increment sequence undefined-behavior c++11

在C++ 11标准的上下文中(不再有序列点的概念,如您所知),我想了解如何定义两个最简单的示例.

int i = 0;

i = i++;   // #0

i = ++i;   // #1
Run Code Online (Sandbox Code Playgroud)

SO上有两个主题可以解释C++ 11环境中的这些示例.这里据说#0调用UB并且#1定义明确.这里有人说,这两个例子是不确定的.这种模棱两可让我很困惑.我已经三次读过这个结构良好的参考文献,但这个主题似乎对我来说太复杂了.

.

让我们分析一下这个例子#0:i = i++;.

相应的报价是:

  • 内置后增量和后减量运算符的值计算在其副作用之前进行排序.

  • 内置赋值运算符和所有内置复合赋值运算符的副作用(左参数的修改)在左右参数的值计算(但不是副作用)之后排序,并在之前排序赋值表达式的值计算(即,在返回对修改对象的引用之前)

  • 如果相对于同一标量对象的另一个副作用,标量对象的副作用未被排序,则行为未定义.

当我得到它时,赋值运算符的副作用没有按其左右参数的副作用排序.因此,赋值运算符的副作用没有按副作用排序i++.所以#0调用一个UB.

.

让我们分析一下这个例子#1:i = ++i;.

相应的报价是:

  • 内置preincrement和predecrement运算符的副作用在其值计算之前被排序(由于定义为复合赋值的隐式规则)

  • 内置赋值运算符和所有内置复合赋值运算符的副作用(左参数的修改)在左右参数的值计算(但不是副作用)之后排序,并在之前排序赋值表达式的值计算(即,在返回对修改对象的引用之前)

  • 如果相对于同一标量对象的另一个副作用,标量对象的副作用未被排序,则行为未定义.

我看不出,这个例子是怎样的#0.对于我而言,这似乎是一个UB,原因与此相同#0.分配的副作用没有按照副作用排序++i.它似乎是一个UB.上面讨论的主题说它定义明确.为什么?

.

问题:如何应用引用规则来确定示例的UB.一个尽可能简单的解释将不胜感激.谢谢!

MWi*_*Wid 9

由于您的报价不是直接来自标准,我将尝试给出详细的答案,引用标准的相关部分."副作用"和"评价"的定义见第1.9/12段:

访问由volatile glvalue(3.10)指定的对象,修改对象,调用库I/O函数或调用执行任何这些操作的函数都是副作用,这些都是执行环境状态的变化.表达式(或子表达式)的评估通常包括值计算(包括确定用于glvalue评估的对象的身份以及获取先前分配给用于prvalue评估的对象的值)和启动副作用.

下一个相关部分是第1.9/15段:

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

现在让我们看看如何将这个应用于这两个例子.

i = i++;
Run Code Online (Sandbox Code Playgroud)

这是增量的后缀形式,您可以在5.2.6中找到它的定义.最相关的句子如下:

在修改操作数对象之前,对++表达式的值计算进行排序.

对于赋值表达式,请参见第5.17段.相关部分指出:

在所有情况下,在右和左操作数的值计算之后,以及在赋值表达式的值计算之前,对赋值进行排序.

使用上面的所有信息,整个表达式的评估是(标准不保证这个顺序!):

  • 价值计算i++(右手边)
  • 值的计算i(左手边)
  • 修改i(副作用++)
  • 修改i(副作用=)

所有标准保证是在赋值表达式的值计算之前对两个操作数的值计算进行排序.但是右手边的值计算只是"读取值i"并且没有修改i,两个修改(副作用)没有相互排序,我们得到了不确定的行为.

第二个例子怎么样?

i = ++i;
Run Code Online (Sandbox Code Playgroud)

这里的情况大不相同.您可以在第5.3.2节中找到前缀增量的定义.相关部分是:

如果x不是bool类型,则表达式++ x等效于x + = 1.

取而代之,我们的表达相当于

i = (i += 1)
Run Code Online (Sandbox Code Playgroud)

仰望复合赋值操作符+=在5.17/7,我们得到的是i += 1相当于i = i + 1,除了i只计算一次.因此,最终成为问题的表达

i =(i =(i + 1))

但是我们已经从上面已经知道,=在值计算之前对操作数的值计算和副作用进行排序后的值计算=.因此,我们得到了明确定义的评估顺序:

  1. 计算i + 1(和i- 内表达的左手边)的值(#1)
  2. 启动内在的副作用=,即修改"内在"i
  3. 计算值(i = i + 1),这是"新"值i
  4. 启动外部的副作用=,即修改"外部"i
  5. 计算完整表达式的值.

(#1):在这里,i只计算一次,因为i += 1等效于i = i + 1不同的是i只计算一次(5.17/7).


Jam*_*nze 8

关键的区别在于++i定义为i += 1,所以

i = ++i;
Run Code Online (Sandbox Code Playgroud)

是相同的:

i = (i += 1);
Run Code Online (Sandbox Code Playgroud)

由于+=操作符的副作用在操作符的值计算之前被排序,因此在外部赋值之前对iin 的实际修改进行了++i排序.这直接来自您引用的部分:"内置赋值运算符和所有内置复合赋值运算符的副作用(左参数的修改)在值计算(但不是副作用)之后排序.左右参数,并在赋值表达式的值计算之前(即,在返回对修改对象的引用之前)进行排序"

这是由于嵌套赋值运算符; (外部)赋值运算符仅对其操作数的值计算进行排序,而不是对其副作用进行排序.(但是,当然,它不会撤消其他方面的排序.)

正如你间接指出的那样,这对C++ 11来说是新的; 以前,两者都未定义.旧版本的C++使用序列点,而不是之前排序,并且任何赋值运算符都没有序列点.(我的印象是,导致左值的运算符具有在任何副作用之后排序的值.在早期的C++中,表达式*&++i是未定义的行为;在C++ 11中,它保证是相同的作为 ++i.)

  • @JamesKanze我不是那个,而是关于`*&++ i`.这不会两次修改`i`. (4认同)
  • @ JohannesSchaub-litb关于`*&++ i`在C++ 11之前未定义的后续问题:/sf/ask/1992558501/ C03 (3认同)