奇怪的std::map行为

50

以下测试程序

#include <map>
#include <iostream>

using namespace std;

int main(int argc, char **argv)
{
    map<int,int> a;
    a[1]=a.size();
    for(map<int,int>::const_iterator it=a.begin(); it!=a.end(); ++it)
            cout << "first " << (*it).first << " second " << (*it).second << endl;
}

g++ 4.8.1(Ubuntu 12.04 LTS)编译时,会导致不同的输出:

g++ xxx.cpp 
./a.out 
first 1 second 1

在Visual Studio 2012(Windows 7)(标准Win32控制台应用程序项目)上:

ConsoleApplication1.exe
first 1 second 0

哪个编译器是正确的?我做错了什么吗?


10
至少会出现未指定的行为。哪个成员函数会首先被调用? - Oliver Charlesworth
出于好奇,优化级别对此有任何影响吗? - ssube
3个回答

77

这实际上是一个形式良好的程序,有两个等效的执行路径,所以两个编译器都是正确的。

a[1] = a.size()
在这个表达式中,= 的两个操作数的评估是无序的。
 

§1.9/15 [intro.execution] 除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的求值都是未排序的。

但是,函数调用不会交错,因此对 operator[]size 的调用实际上是 不确定有序,而不是无序的。

 

§1.9/15 [intro.execution] 在调用函数的函数中(包括其他函数调用)中没有被明确排序在调用函数体执行之前或之后的每个评估与调用函数的执行不确定排序。

这意味着函数调用可能以以下两种顺序之一发生:

  1. operator[] 然后是 size
  2. size 然后是 operator[]

如果一个键不存在,并且您使用该键调用 operator[],它将添加到映射中,从而改变映射的大小。 因此,在第一种情况下,将添加键,检索大小(现在为1),并将 1 赋值给该键。 在第二种情况下,将检索大小(为0),添加键,并将 0 赋值给该键。

请注意,这不是导致未定义行为的情况。 当两个修改或一个标量对象的修改和读取是无序的时,会发生未定义行为。

 

§1.9/15 [intro.execution] 如果与同一标量对象的另一个副作用或使用同一标量对象的值进行值计算的副作用无序,则行为是未定义的。

在这种情况下,它们不是无序的,而是不确定有序的。

因此,我们拥有程序执行的两个等效有效顺序。 任何一个都可能发生并且都会产生有效输出。 这是未指定的行为

 

§1.3.25 [defns.unspecified]
  未指定的行为
  对于正确的数据和格式良好的程序构造而言,行为取决于实现


所以回答你的问题:

 

哪个编译器是正确的?

它们两个都是正确的。

 

我做错了什么吗?

可能。 您不太可能想编写具有这两种执行路径的代码。 未指定的行为可能是可以接受的,与未定义的行为不同,因为它可以解析为单个可观察的输出,但如果可以避免,则在第一次就没有必要拥有此类模棱两可的行为。 相反,不要编写具有这种模棱两可性质的代码。 根据您想要的正确路径,您可以执行以下操作之一:

auto size = a.size();
a[1] = size; // value is 0

或者:

a[1];
a[1] = a.size(); // value is 1

如果你想要结果为1,并且你知道这个键还不存在,当然可以使用第一段代码但是将size + 1赋值。


14
你是唯一一个提到这里有函数调用对结果产生影响的人,这使得这个行为未指定而非未定义。+1 - juanchopanza

15
在这种情况下,当a[1]返回一个基本类型时,请参考这个答案。如果std::map的值类型是用户定义的类型,并且为该类型定义了operator=(T, std::size_t),则表达式:
a[1] = a.size();

可以转换为相应的不带糖语法版本:

a[1] = a.size();
a.operator[](1) = a.size();
operator=(a.operator[](1), a.size());

正如我们都知道的来自§8.3.6/9的:

函数参数的求值顺序是未指定的。

这导致上述表达式的结果是未指定的

当然,我们有两种情况:

  • 如果先评估 a.operator[](1),则映射的大小将增加1,导致第一个输出 (first 1 second 1)。
  • 如果先评估 a.size(),则您将获得第二个输出 (first 1 second 0)。

请注意,这里涉及的类型都是原始类型,因此这里没有隐式的 operator= 函数调用。 - Oliver Charlesworth
1
这很接近,但它与函数参数的评估顺序无关(因为这里使用的=不是函数调用)。相反,它是关于函数调用的不确定序列,因为它们不能交错。 - Joseph Mansfield
@sftrabbit,当然你应该得到采纳的答案。我只是把这里留作参考,以防std::map的值类型不是原始类型的情况。如果您希望更正或删除此帖,请告诉我。 - Shoe

14
这是一个sequence-point问题,这意味着编译器可以按任意顺序执行某些操作。
如果其中一个操作对另一个操作产生了副作用,则称为“未指定的行为”,有点像“未定义的行为”,但结果必须是固定子集中的一个,因此它必须是0或1,不能是其他值。在实际情况下,通常应避免这样做。
在您的特定情况下,在映射上执行operator []会更改其大小(如果该元素尚不存在)。因此,它对其分配给右侧具有副作用。

7
当我想重新开始使用C++开发时,我会遇到类似这样的问题,然后再次慢慢地退缩。我欣赏给编译器自由进行优化的机会,但这个问题真的非常棘手。 - Matthew Walton
4
@MatthewWalton你会在另一种语言中真的像这样做吗 a[1]=a.size();(真诚提问,我会避免这样做,但这可能是我的C++偏见)? - juanchopanza
7
不是未定义,只是真正未指定,这意味着标准基本上表示“所有可能的执行序列都可以,结果必须是从这些序列中产生的一个”。但它对哪一个结果也没有限制 - 实际上,如果它在不同的执行中发生变化,这也是有效的。 - Sebastian Redl
2
@juanchopanza 永远不要这样做。a[1]=a.size() 的意思是“在它有效之前立即使用后置条件来执行此操作”。 - Paul Evans
2
@juanchopanza:当然。例如,在Python中,a[1] = a.size()大致相当于a.__setitem__(1, a.size()),这样做的语义完全正常。但这是因为Python不允许您像C++那样返回可分配的引用,因此a.x() = a.y()不是有效的构造。索引赋值(a[1] = ...)和属性赋值(a.foo = ...)是协议特殊处理的,参见__setitem____setattr__ - Florian Brucker
显示剩余9条评论

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