18

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

int i = 0;

i = i++;   // #0

i = ++i;   // #1

有两个关于 SO 的主题在 C++11 上下文中解释了这些示例。据说这里#0调用了 UB 并且#1定义明确。据说这两个例子都是未定义的这种模棱两可让我很困惑。我已经将这篇结构良好的参考资料读了三遍,但这个话题对我来说似乎太复杂了。

.

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

相应的报价是:

  • 内置 postincrement 和 postdecrement 运算符的值计算在其副作用之前排序。

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

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

据我所知,赋值运算符的副作用与它的左右参数的副作用没有顺序。因此,赋值运算符的副作用与i++. 所以#0调用了一个UB。

.

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

相应的报价是:

  • 内置预递增和预递减运算符的副作用在其值计算之前排序(由于定义为复合赋值的隐含规则)

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

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

我看不出,这个例子与#0. 这对我来说似乎是一个 UB,原因与#0. 赋值的副作用与++i. 好像是个UB。上面喜欢的主题说它定义明确。为什么?

.

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

4

2 回答 2

9

由于您的报价并非直接来自标准,因此我将尝试引用标准的相关部分给出详细的答案。“副作用”和“评估”的定义见第 1.9/12 段:

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

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

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

现在让我们看看,如何将其应用于这两个示例。

i = i++;

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

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

有关赋值表达式,请参见第 5.17 段。相关部分指出:

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

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

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

所有标准保证是两个操作数的值计算在赋值表达式的值计算之前排序。但是右侧的值计算只是“读取值i”而不是修改i,这两个修改(副作用)没有相互排序,我们得到未定义的行为。

第二个例子呢?

i = ++i;

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

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

代之以,我们的表达式等价于

i = (i += 1)

查找+=5.17/7 中的复合赋值运算符,我们得到它i += 1等价于i = i + 1除了i只计算一次。因此,有问题的表达式最终变为

我 = ( 我 = (我 + 1))

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

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

(#1): 这里,i只被评估一次,因为i += 1相当于i = i + 1except thati只被评估一次 (5.17/7)。

于 2013-07-01T11:32:03.653 回答
8

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

i = ++i;

是相同的:

i = (i += 1);

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

这是由于嵌套的赋值运算符;(外部)赋值运算符仅在其操作数的值计算上施加 sequenced before,而不是在它们的副作用上。(但当然,它不会撤消以其他方式强加的排序。)

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

于 2013-07-01T08:37:48.820 回答