奇怪的std :: map行为

Wal*_*ald 50 c++ g++ visual-studio-2012

以下测试程序

#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;
}
Run Code Online (Sandbox Code Playgroud)

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

g++ xxx.cpp 
./a.out 
first 1 second 1
Run Code Online (Sandbox Code Playgroud)

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

ConsoleApplication1.exe
first 1 second 0
Run Code Online (Sandbox Code Playgroud)

哪个编译器是对的?难道我做错了什么?

Jos*_*eld 77

这实际上是一个格式良好的程序,它有两个同样有效的执行路径,因此两个编译器都是正确的.

a[1] = a.size()
Run Code Online (Sandbox Code Playgroud)

在这个表达式中,对两个操作数的评估=是不确定的.

§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
Run Code Online (Sandbox Code Playgroud)

要么:

a[1];
a[1] = a.size(); // value is 1
Run Code Online (Sandbox Code Playgroud)

如果你想要结果1并且你知道密钥还不存在,你当然可以做第一个代码但是分配size + 1.

  • +1你是唯一一个在这里提到函数调用的效果的人,这使得这个未指定而非未定义的行为有所不同. (14认同)

Sho*_*hoe 15

在这种情况下,a[1]返回基本类型,请参阅此答案.如果std::map's值类型是用户定义的类型并且operator=(T, std::size_t)是为该类型定义的,则表达式为:

a[1] = a.size();
Run Code Online (Sandbox Code Playgroud)

可以转换为相应的less-syntactic-sugar版本:

a[1] = a.size();
a.operator[](1) = a.size();
operator=(a.operator[](1), a.size());
Run Code Online (Sandbox Code Playgroud)

而且,正如我们从§8.3.6/ 9中所知道的那样:

函数参数的评估顺序未指定.

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

当然,我们有两种情况:

  • 如果a.operator[](1)首先计算,则地图的大小增加1,从而导致第一个输出(first 1 second 1).
  • 如果a.size()首先评估,那么你得到的输出是第二个(first 1 second 0).


Cas*_*Cow 14

这被称为序列点问题,这意味着可以按编译器选择的任何顺序执行某些操作.

如果一个人对另一个人有副作用,那么它被称为"未指定的行为",有点像"未定义的行为",但是结果必须是结果的固定子集之一,所以这里必须是0或1并且可以'是任何其他价值.实际上你通常应该避免这样做.

在你的特殊情况下.operator []在地图上执行会更改其大小(如果该元素尚不存在).因此,它在分配给它的右侧有副作用.

  • 当我想回到C++开发时,我会遇到类似这样的东西然后再慢慢退回去.我很欣赏赋予编译器自由优化功能,但这个版本真的是底部的. (7认同)
  • 这不是未定义的.它只是真正未指定,这意味着标准基本上说"所有可能的执行顺序都很好,结果必须是来自这些序列的那些".但它对哪一个没有限制 - 事实上,如果它从执行变为执行,它将是有效的. (7认同)
  • @MatthewWalton你真的会做一些像'a [1] = a.size();`用另一种语言(真正的问题,我避免它,但它可能是我的C++偏见)? (4认同)
  • @juanchopanza从不.`a [1] = a.size()`说"在它有效之前立即对这个非常操作使用后置条件" (2认同)
  • @juanchopanza:当然可以.例如,在Python中,a [1] = a.size()`大致转换为`a .__ setitem __(1,a.size())`,它具有完美的语义.但那是因为Python不允许你像C++那样返回一个可赋值的引用,所以`ax()= ay()`不是一个有效的构造.索引赋值(`a [1] = ...`)是协议特殊的,属性赋值(`a.foo = ...`)(参见[`__setitem__`](http://docs.python) .org/2/reference/datamodel.html#object .__ setitem__)和[`__setattr ___](http://docs.python.org/2/reference/datamodel.html#object.__setattr__)). (2认同)