奇怪的std :: map行为
以下testing程序
#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
哪个编译器是正确的? 难道我做错了什么?
这实际上是一个格式良好的程序,它有两个同样有效的执行path,所以两个编译器都是正确的。
a[1] = a.size()
在这个expression式中, =
的两个操作数的评估是不确定的。
§1.9/ 15 [intro.execution]除非另有说明,个别运算符和个别expression式的操作数的评估是不确定的 。
然而函数调用并不是交错的,所以对operator[]
和size
的调用实际上是不确定的 ,而不是无序的。
§1.9/ 15 [intro.execution]调用函数的主体函数(包括其他函数调用)的每个评估,在执行被调用函数的主体之前或之后,都没有被特定地sorting,对被调用函数的执行是不确定的function。
这意味着函数调用可能以两个命令之一发生:
-
operator[]
然后size
-
size
然后operator[]
如果一个键不存在,并用该键调用operator[]
,它将被添加到地图,从而改变地图的大小。 因此,在第一种情况下,密钥将被添加,大小将被检索(现在是1),并且1
将被分配给该密钥。 在第二种情况下,大小将被检索(这是0),密钥将被添加,并且0
将被分配给该密钥。
请注意,这不是导致未定义行为的情况。 未定义的行为发生在对相同标量对象的两个修改或修改和读取不被确定时。
§1.9/ 15 [intro.execution]如果一个标量对象上的边的效应不是相对于同一个标量对象上的另一边效应或者是使用同一个标量对象的值计算的,那么这个行为是不确定的。
在这种情况下,它们并不是没有确定性,而是不确定的。
所以我们所做的是两个同样有效的程序执行顺序。 要么可能发生,并且都会给出有效的输出。 这是不明确的行为 。
§1.3.25[defns.unspecified]
未明确的行为
行为,一个结构良好的程序构造和正确的数据,这取决于实现
所以要回答你的问题:
哪个编译器是正确的?
他们都是。
难道我做错了什么?
大概。 你不想编写有两个这样的执行path的代码。 未定义的行为可以是好的,不像未定义的行为,因为它可以parsing为单个可观察的输出,但是如果可以避免的话,首先不值得。 相反,不要编写这种含糊不清的代码。 根据你想要的正确path,你可以执行以下任一操作:
auto size = a.size(); a[1] = size; // value is 0
要么:
a[1]; a[1] = a.size(); // value is 1
如果你想要结果是1
,你知道键还不存在,你当然可以做第一个代码,但分配size + 1
。
在这种情况下, a[1]
返回一个原始types,请参考这个答案 。 如果std::map
的值types是用户定义的types,而operator=(T, std::size_t)
是为该types定义的,则expression式:
a[1] = a.size();
可以转换成相应的较less语法糖的版本:
a[1] = a.size(); a.operator[](1) = a.size(); operator=(a.operator[](1), a.size());
而且,正如我们在§8.3.6/ 9中所知道的那样:
函数参数的评估顺序是未指定的。
这导致了上述expression式的结果未被指定的事实。
当然,我们有两种情况:
- 如果首先评估
a.operator[](1)
,则映射的大小增加1,导致第一个输出(first 1 second 1
)。 - 如果首先计算
a.size()
,则会得到的输出是第二个(first 1 second 0
)。
这被称为序列点问题,这意味着某些操作可能以编译器select的任何顺序执行。
如果另一方有副作用,则称之为“未定义的行为”,有点像“未定义的行为”,但结果必须是结果的固定子集之一,所以在这里它必须是0或1,没有任何其他价值。 事实上,你通常应该避免这样做。
在你的具体情况。 在地图上执行operator []
改变其大小(如果该元素尚不存在)。 因此它在分配给它的右边有副作用。