C++11标准委员会是否打算让unordered_map在插入后立即销毁其内容?

117
我刚刚浪费了三天的时间追踪一个非常奇怪的错误,即unordered_map::insert()会破坏你插入的变量。这种高度不明显的行为只发生在最近的编译器中:我发现clang 3.2-3.4和GCC 4.8是唯一展示这个“特性”的编译器。
以下是我主要代码库中的一些简化代码,用于演示这个问题:
#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,并请求他们实现更好的行为。

10
只因为它使用了通用引用并不意味着插入的值总是会被移动 - 它只应该对右值进行移动,而普通的 a 不是右值。它应该制作一份副本。此外,这种行为完全取决于标准库,而不是编译器。 - Xeo
10
似乎这是库实现中的一个错误。 - David Rodríguez - dribeas
5
因此,根据C++11标准,插入销毁行为实际上是正确的,旧的编译器是不正确的。很抱歉,但你是错误的。你从C++标准的哪个部分得到这个想法?顺便说一句,cplusplus.com不是官方网站。 - Ben Voigt
1
我无法在我的系统上重现这个问题,我分别使用gcc 4.8.2和4.9.0 20131223 (experimental)。对我来说,输出是a.second is now 0x2074088(或类似的内容)。 - user1508519
48
这是GCC的一个错误,编号为57619(http://gcc.gnu.org/bugzilla/show_bug.cgi?id=57619)。这是4.8系列中的一个回归问题,在2013年6月的4.8.2版本中得到了修复。 - Casey
显示剩余3条评论
2个回答

85

正如其他评论中指出的那样,“通用”构造函数并不总是从其参数中移动。如果参数确实是一个右值,它应该移动,如果是一个左值,则应该复制。

您观察到的行为始终移动的情况是 libstdc++ 中的一个错误,根据问题的评论现在已经得到修复。对于那些好奇的人,我看了一下 g++-4.8 的头文件。

bits/stl_map.h,598-603 行

  template<typename _Pair, typename = typename
           std::enable_if<std::is_constructible<value_type,
                                                _Pair&&>::value>::type>
    std::pair<iterator, bool>
    insert(_Pair&& __x)
    { return _M_t._M_insert_unique(std::forward<_Pair>(__x)); }

bits/unordered_map.h,第365-370行

  template<typename _Pair, typename = typename
           std::enable_if<std::is_constructible<value_type,
                                                _Pair&&>::value>::type>
    std::pair<iterator, bool>
    insert(_Pair&& __x)
    { return _M_h.insert(std::move(__x)); }

后者在使用std::move时错误地应该使用std::forward


12
Clang 默认使用 libstdc++,而 GCC 使用的是 stdlib。 - Xeo
是的,我想他们修复了这个漏洞。 - Brian Bi
@Brian 不,我刚刚检查了我的系统头文件,情况一样。顺便说一下,版本是4.8.2。 - user1508519
@Brian,ideone也使用4.8.1,并且没有重现这个错误。但是coliru却有。Rextester也没有重现这个错误(不确定编译器版本)。编辑:它使用4.7.3,使其成为一个“旧”的编译器的好选择。 - user1508519
@BrianBi,在同一个文件中是否有std::unordered_map ::insert()的const ref重载可用? - dhruvbird
显示剩余3条评论

20
template <class P> pair<iterator,bool> insert ( P&& val );

这是一个贪婪的通用引用移动重载,它会消耗不符合其他重载的任何内容,并将其移动构造为值类型。
这就是有些人所谓的“通用引用”,但实际上是“引用折叠”。在您的情况下,如果参数是类型为pair>的左值,则不会导致参数成为右值引用,也不应该从中移动。
那么为什么我们上面的代码选择了这个重载,而不是unordered_map::value_type重载,这可能是大多数人期望的呢?
因为像你以及之前许多人一样,误解了容器中的value_type。*map(无论是有序还是无序)的value_type是pair,在您的情况下是pair>。类型不匹配会消除您可能期望的重载。
iterator       insert(const_iterator hint, const value_type& obj);

我仍然不明白为什么这个新引理存在,“通用引用”,它并没有具体的意义,也没有一个好的名称,因为在实践中它完全不是“通用”的。最好记住一些旧的折叠规则,它们是C++元语言行为的一部分,自从模板被引入该语言以来就一直存在,再加上C++11标准中的一个新签名。无论如何,谈论这些通用引用有什么好处呢?已经有足够奇怪的名称和事物了,比如std::move根本不移动任何东西。 - user2485710
2
有时候,无知是福,而在所有引用折叠和模板类型推断调整中使用“通用引用”这个总称,我认为这相当不直观,再加上需要使用std::forward来使该调整发挥真正的作用... Scott Meyers已经为转发(使用通用引用)制定了相当简单明了的规则,做得很好。 - Mark Garcia
1
在我看来,“通用引用”是一种声明函数模板参数的模式,可以绑定到左值和右值。在“通用引用”情况和其他上下文中,当将(推导或指定的)模板参数替换为定义时,就会发生“引用折叠”。 - aschepler
2
@aschepler:通用引用只是对引用折叠的一个花哨名称而已。我同意"无知即是福"的观点,也认为给它取个花哨的名字可以更轻松、更时髦地谈论它,这可能有助于传播这种行为。话虽如此,我并不是特别喜欢这个名字,因为它将实际规则推到了一个不需要被“了解”的角落……直到你遇到了Scott Meyers所描述的案例之外的情况。 - David Rodríguez - dribeas
现在看来这些语义上的区别可能有点无意义,但我想要表达的是:当我设计并声明一个函数参数为可推导模板参数 && 时,会发生通用引用;当编译器实例化一个模板时,会发生引用折叠。引用折叠是通用引用能够工作的原因,但我的大脑不喜欢将这两个术语放在同一领域内。 - aschepler
@aschepler:我能理解你的推理,尽管你可能对const的折叠没有任何问题,对吧?template <typename T> void f(const T&);作为f<const int>(1)调用,你也不会称之为universal-const…但我理解这个例子似乎不同,因为&&的新颖性,直到我们更熟悉它之前,将其分解成小片段有助于更好地理解这些概念。 - David Rodríguez - dribeas

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