以下测试程序
#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
哪个编译器是对的?我做错了吗?
答案 0 :(得分:77)
这实际上是一个结构良好的程序,它有两个同样有效的执行路径,因此两个编译器都是正确的。
a[1] = a.size()
在此表达式中,=
的两个操作数的评估未被排除。
§1.9/ 15 [intro.execution] 除非另有说明,否则对单个运算符的操作数和单个表达式的子表达式的评估都是无效的。
但是,函数调用不是交错的,因此对operator[]
和size
的调用实际上是不确定地排序,而不是未经过排序。
§1.9/ 15 [intro.execution] 在执行被调用函数体之前或之后,在调用函数(包括其他函数调用)中的每个评估都没有特别顺序排序。关于被调用函数的执行,不确定地排序。
这意味着函数调用可能以两个顺序之一发生:
operator[]
然后size
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
。
答案 1 :(得分:15)
在这种情况下,a[1]
返回基本类型,请参阅this answer。如果std::map
的值类型是用户定义的类型,并且为该类型定义了operator=(T, std::size_t)
,则表达式为:
a[1] = a.size();
可以转换为相应的less-syntactic-sugar版本:
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
)。答案 2 :(得分:14)
这称为sequence-point问题,这意味着某些操作可以按编译器选择的任何顺序执行。
如果一方对另一方有副作用,则称其为“未指定行为”,有点像“未定义行为”,但结果必须是结果的固定子集之一,所以这里必须为0或1并且不能是任何其他价值。实际上,你通常应该避免这样做。
在您的特定情况下。在地图上执行operator []
会更改其大小(如果该元素尚不存在)。因此,它在分配给它的右侧有副作用。