【问题标题】:Order of evaluation and undefined behaviour评估顺序和未定义行为
【发布时间】:2013-06-28 07:38:27
【问题描述】:

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

int i = 0;

i = i++;   // #0

i = ++i;   // #1

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

.

我们来分析例子#0:i = i++;

相应的引号是:

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

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

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

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

.

我们来分析例子#1:i = ++i;

相应的引号是:

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

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

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

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

.

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

【问题讨论】:

  • 如果我在生产代码中看到这一点,我会与程序员进行一次非常坦率的对话。是的,这是一种智力上的好奇心,但在思考这个问题时看不到太多好处。也就是说,对于写得很好的问题+1,我将详细阅读已接受的答案。
  • @Bathsheba 我同意这些代码示例可能永远不会出现在真实代码中,但我认为对标准的理解将有助于确定其他示例中的 UB。
  • 我对你的问题质量表示赞同,并且和你一样,等待类似质量的答案。
  • @Bathsheba 谢谢你,希望我们能在这里看到一些全面的解释。
  • 讨论完成后请有人将其添加到社区答案中

标签: c++ c++11 sequence increment undefined-behavior


【解决方案1】:

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

i = ++i;

等同于:

i = (i += 1);

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

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

正如您间接指出的,这是 C++11 的新内容; 以前,两者都是未定义的。旧版本的 C++ 使用序列点,而不是之前排序,并且有 在任何赋值运算符中都没有序列点。 (一世 有这样的印象,即意图是运营商 导致左值有一个值,该值在任何 副作用。在早期的 C++ 中,表达式 *&++i 是 未定义的行为;在 C++11 中,保证与 ++i.)

【讨论】:

  • 感谢詹姆斯的回答。您能否解释一下为什么这是真的,可能有一些参考资料? Since the side effects of the += operator are sequenced before the value computation of the operator, the actual modification of i in ++i is sequenced before the outer assignment.
  • 我的问题中的引用指出i = (i += 1); 中第一个赋值的副作用与它的正确参数的副作用没有顺序 - (i += 1)。我理解错了吗?
  • 我不知道是不是只有我不够聪明才能理解它,但可能一些非常详细的解释会对我和其他一些人有所帮助。
  • @JamesKanze 我不是关于那个,而是关于*&++i。这不会修改i 两次。
  • @JohannesSchaub-litb 关于*&++i 在 C++11 之前未定义的后续问题:stackoverflow.com/questions/28465121/…
【解决方案2】:

由于您的报价并非直接来自标准,因此我将尝试引用标准的相关部分给出详细的答案。 “副作用”和“评估”的定义见第 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 只计算一次。因此,有问题的表达式最终变为

i = (i = (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 + 1,除了 i 只计算一次 (5.17/7)。

【讨论】:

  • 感谢您的详细解答。如果我理解正确,则第二个示例的完整描述的评估顺序是:计算内部赋值的参数 ==> 内部赋值的副作用 ==> 计算内部赋值的值 ==> 外部赋值的副作用 ==> 计算外部分配。
  • @Kolyunya 我更新了我的答案。我希望这能回答你的问题。
  • 谢谢。我想我确实明白了。
  • @MWid 我认为左手边i 的值计算没有确定,因为左操作数和右操作数的值计算是不确定的,是在之前还是之后?所以对“++i”的修改和i(左手)的值计算是无序的,它会调用UB,但实际上“i=++i”是格式正确的,所以我认为没有值计算“我”(左手)
猜你喜欢
  • 2016-12-03
  • 2010-11-24
  • 1970-01-01
  • 2011-05-23
  • 2014-04-14
  • 1970-01-01
  • 2013-03-18
  • 2011-06-05
  • 1970-01-01
相关资源
最近更新 更多