C++中赋值语句的求值顺序

18
map<int, int> mp;
printf("%d ", mp.size());
mp[10]=mp.size();
printf("%d\n", mp[10]);

这段代码的输出并不是很直观:

0 1

我知道为什么会这样 - 赋值语句左侧返回对 mp[10] 所在值的引用,同时创建了该值,在此之后才计算右侧表达式,使用 map 新计算出的 size()

C++ 标准文档中有没有明确说明这种行为?或者说评估的顺序是否未定义?

这个结果是使用 g++ 5.2.1 得到的。


这真的很奇怪。gcc 4.8.4 也返回0 1,但是clang 3.4 返回0 0。 - Harald
4
请注意 "undefined" 这个词的使用。它与 "unspecified" 不是同义词。在使用时需谨慎。 - molbdnilo
补充redcrash的说法,截至上周五晚上,gcc 4.9.2和clang 3.8都遵循了4.8.4和3.4的结果。进一步证明这并不需要特定的顺序。 - Mats Petersson
1
有趣的问题。请看这里 - erip
@erip:是的,这并不奇怪,因为在调用map::insert之前必须完成std::pair的构造函数。 - Mats Petersson
4个回答

18

是的,这在标准中有所涵盖,但它是未指定的行为。最近的C++标准提案N4228: Refining Expression Evaluation Order for Idiomatic C++涵盖了这种情况,旨在完善评估顺序规则,使其对某些情况进行明确定义。

它描述了这个问题如下:

Expression evaluation order is a recurring discussion topic in the C++ community. In a nutshell, given an expression such as f(a, b, c), the order in which the sub-expressions f, a, b, c are evaluated is left unspecified by the standard. If any two of these sub-expressions happen to modify the same object without intervening sequence points, the behavior of the program is undefined. For instance, the expression f(i++, i) where i is an integer variable leads to undefined behavior , as does v[i] = i++. Even when the behavior is not undefined, the result of evaluating an expression can still be anybody’s guess. Consider the following program fragment:

#include <map>

int main() {
  std::map<int, int>  m;
  m[0] = m.size(); // #1
}

What should the map object m look like after evaluation of the statement marked #1? { {0, 0 } } or {{0, 1 } } ?

我们知道,如果没有指定子表达式的评估顺序,则其是无序的。这来自于draft C++11 standard1.9程序执行,其中写道:

除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的求值是无序的。[...]

而所有的5.17节赋值和复合赋值运算符[expr.ass]所说的是:

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

因此,这一节并没有确定评估顺序,但我们知道这不是未定义行为,因为operator []size()都是函数调用,而第1.9节告诉我们(我强调):
当调用函数(无论函数是否内联)时,与指定调用的后缀表达式或被调用函数相关的所有参数表达式的值计算和副作用都在调用函数主体中的每个表达式或语句执行之前排序。[注意:不同参数表达式相关的值计算和副作用是未排序的。——end note] 在调用函数的主体执行之前,与调用函数相关的每个表达式或语句中没有明确定序的评估与被调用函数的执行顺序不确定地排序。9 [...]
请注意,我在问题“The C++ Programming Language”第4版第36.3.6节中的此代码是否具有定义良好的行为?中涵盖了提案N4228的第二个有趣的示例。

更新

我看起来像是 N4228 的修订版本在最近的WG21会议上被进化工作组接受, 但该论文(P0145R0)还没有公开。因此,这可能不再是C++17中未指定的内容。

更新2

P0145的第三版将其指定,并更新了 [expr.ass]p1:

赋值运算符(=)和复合赋值运算符都是从右到左进行分组的。 它们的左操作数必须是可修改的左值;它们的结果是引用左操作数的左值。 如果左操作数是位域,则在所有情况下结果都是位域。 在所有情况下,赋值在右操作数和左操作数的值计算之后进行序列化,并在赋值表达式的值计算之前进行序列化。右操作数在左操作数之前进行序列化。 ...

1
请注意,为了表明这是未指定的行为而不是未定义的行为,我们需要引用第1.9节,引用第5.17节是不够的。 - Shafik Yaghmour
1
请注意,根据提案p0145r3,这不再是未指定的行为。现在§[expr.ass]中写道:“右操作数在左操作数之前被排序”。 - Shafik Yaghmour
如果有这样的表达式 *f(foo()) = x; 或者 *(foo()) += x;,其中 x 可能可以被函数 foo 访问到,那么编译器是否需要读取 x,将其保存在某个地方,然后调用 foo,检索保存的值,然后将其存储或添加到 foo 中呢?这似乎会增加一些平台上的开销,在我测试过的编译器中,只有 clang 在 C++17 模式下实际上是这样工作的。 - supercat

8
根据C ++ 11标准(重点在此):
5.17 赋值和复合赋值运算符
1.赋值运算符(=)和复合赋值运算符的所有组都是从右到左。它们都需要可修改的左值作为它们的左操作数,并返回一个引用左操作数的左值。如果左操作数是位字段,则在所有情况下,结果均为位字段。在所有情况下,赋值都会在对右侧和左侧操作数进行值计算之后进行,并且在赋值表达式的值计算之前进行。 语言未指定评估左操作数还是评估右操作数。编译器可以自由选择先评估哪个操作数。由于您的代码的最终结果取决于操作数的评估顺序,因此我认为这是未指定行为而不是未定义行为。
根据1.3.25未指定行为:
对于良好形成的程序构造和正确数据,取决于实现的行为。

3

我肯定标准没有规定在C++标准中表达式x = y;按照什么顺序进行计算(这就是为什么你不能执行*p++ = *p++,因为p++的执行顺序未定义)。

换句话说,为了保证表达式x = y;的执行顺序被定义,你需要将其分解为两个序列点。

 T tmp = y;
 x = tmp;

当然,在这种特殊情况下,编译器可能更喜欢在执行size()之前执行operator[],因为它可以直接将值存储到operator[]的结果中,而不是将其保留在临时位置中,在operator[]评估后再存储它。但我相信编译器并不需要按照这个顺序执行。

1
你提出的break引入了额外的对象和副作用 - 使用某种引用可能更好。 - M.M
只有当 y 不是像 intsize_t 这样的平凡类型时才成立。 - Mats Petersson

1
让我们来看一下你的代码分解成了什么:
mp.operator[](10).operator=(mp.size());

这段文字基本上讲述了,在第一部分创建了一个值为10的条目,而在第二部分将容器的大小分配给10号位置的整数引用。

但现在你进入了未指定的评估顺序问题。这里有一个更简单的示例

map::size()应该在map::operator(int const &)之前还是之后调用?

没有人真正知道。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接