我刚刚浪费了三天的时间追踪一个非常奇怪的错误,即unordered_map::insert()会破坏你插入的变量。这种高度不明显的行为只发生在最近的编译器中:我发现clang 3.2-3.4和GCC 4.8是唯一展示这个“特性”的编译器。
以下是我主要代码库中的一些简化代码,用于演示这个问题:
我,像大多数C++程序员一样,希望输出的结果看起来像这样:
但是使用clang 3.2-3.4和GCC 4.8时,我得到的是这个结果:
这可能没有意义,直到你仔细查看unordered_map::insert()的文档http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/,其中第二个重载是:
哪个是贪婪的通用引用移动重载,它会消耗任何不匹配其他重载的内容,并将其移动构造为value_type。那么为什么我们上面的代码选择了这个重载,而不是unordered_map::value_type重载,这可能是大多数人所期望的呢?
答案就在你面前:unordered_map::value_type是一个pair<const int, std::shared_ptr>,编译器会正确地认为pair<int, std::shared_ptr>是不可转换的。因此,编译器选择了移动通用引用重载,这会销毁原始值,尽管程序员没有使用std::move(),这是表示你可以接受变量被销毁的典型约定。因此,插入时的销毁行为实际上是符合C++11标准的,而旧版本的编译器则是错误的。
你现在可能明白为什么我花了三天时间来诊断这个错误了。在一个庞大的代码库中,插入到unordered_map中的类型是在源代码中远离定义的typedef,这一点并不明显,而且从来没有人想到要检查typedef是否与value_type相同。
所以我在Stack Overflow上有几个问题:
为什么旧版本的编译器不会像新版本的编译器一样销毁插入的变量?我的意思是,即使是GCC 4.7也不会这样做,而且它符合相当多的标准。
这个问题是否被广泛知晓?因为升级编译器肯定会导致之前正常工作的代码突然停止工作。
C++标准委员会是否有意这样设计?
你认为应该如何修改unordered_map::insert()以获得更好的行为?我之所以问这个问题,是因为如果有支持的话,我打算将这个行为作为一个N注释提交给WG21,并请求他们实现更好的行为。
以下是我主要代码库中的一些简化代码,用于演示这个问题:
#include <memory>
#include <unordered_map>
#include <iostream>
int main(void)
{
std::unordered_map<int, std::shared_ptr<int>> map;
auto a(std::make_pair(5, std::make_shared<int>(5)));
std::cout << "a.second is " << a.second.get() << std::endl;
map.insert(a); // Note we are NOT doing insert(std::move(a))
std::cout << "a.second is now " << a.second.get() << std::endl;
return 0;
}
我,像大多数C++程序员一样,希望输出的结果看起来像这样:
a.second is 0x8c14048
a.second is now 0x8c14048
但是使用clang 3.2-3.4和GCC 4.8时,我得到的是这个结果:
a.second is 0xe03088
a.second is now 0
这可能没有意义,直到你仔细查看unordered_map::insert()的文档http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/,其中第二个重载是:
template <class P> pair<iterator,bool> insert ( P&& val );
哪个是贪婪的通用引用移动重载,它会消耗任何不匹配其他重载的内容,并将其移动构造为value_type。那么为什么我们上面的代码选择了这个重载,而不是unordered_map::value_type重载,这可能是大多数人所期望的呢?
答案就在你面前:unordered_map::value_type是一个pair<const int, std::shared_ptr>,编译器会正确地认为pair<int, std::shared_ptr>是不可转换的。因此,编译器选择了移动通用引用重载,这会销毁原始值,尽管程序员没有使用std::move(),这是表示你可以接受变量被销毁的典型约定。因此,插入时的销毁行为实际上是符合C++11标准的,而旧版本的编译器则是错误的。
你现在可能明白为什么我花了三天时间来诊断这个错误了。在一个庞大的代码库中,插入到unordered_map中的类型是在源代码中远离定义的typedef,这一点并不明显,而且从来没有人想到要检查typedef是否与value_type相同。
所以我在Stack Overflow上有几个问题:
为什么旧版本的编译器不会像新版本的编译器一样销毁插入的变量?我的意思是,即使是GCC 4.7也不会这样做,而且它符合相当多的标准。
这个问题是否被广泛知晓?因为升级编译器肯定会导致之前正常工作的代码突然停止工作。
C++标准委员会是否有意这样设计?
你认为应该如何修改unordered_map::insert()以获得更好的行为?我之所以问这个问题,是因为如果有支持的话,我打算将这个行为作为一个N注释提交给WG21,并请求他们实现更好的行为。
a
不是右值。它应该制作一份副本。此外,这种行为完全取决于标准库,而不是编译器。 - Xeo4.9.0 20131223 (experimental)
。对我来说,输出是a.second is now 0x2074088
(或类似的内容)。 - user1508519