C++ 标准委员会是不是打算在 C++11 中 unordered_map 破坏它插入的内容?

Posted

技术标签:

【中文标题】C++ 标准委员会是不是打算在 C++11 中 unordered_map 破坏它插入的内容?【英文标题】:Is it intended by the C++ standards committee that in C++11 unordered_map destroys what it inserts?C++ 标准委员会是否打算在 C++11 中 unordered_map 破坏它插入的内容? 【发布时间】:2014-02-26 12:32:59 【问题描述】:

我刚刚失去了三天的生命来追踪一个非常奇怪的错误,其中 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

这可能没有意义,直到您仔细检查 http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/ 的 unordered_map::insert() 文档,其中第 2 号重载是:

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

这是一个贪婪的通用引用移动重载,消耗与任何其他重载不匹配的任何内容,并将其移动构造到 value_type。那么为什么我们上面的代码选择了这个重载,而不是大多数人所期望的 unordered_map::value_type 重载呢?

答案让你眼前一亮:unordered_map::value_type 是一对const int, std::shared_ptr> 编译器会正确地认为一对int em>, std::shared_ptr> 不可转换。因此,编译器选择移动通用引用重载,这会破坏原始的,尽管程序员不使用 std::move() 这是表明您可以接受变量被破坏的典型约定。因此,根据 C++11 标准,插入破坏行为实际上是正确的,而旧的编译器是不正确的

您现在可能明白为什么我花了三天时间来诊断这个错误。在插入到 unordered_map 中的类型是在源代码术语中定义得很远的 typedef 的大型代码库中一点也不明显,而且任何人从未想过检查 typedef 是否与 value_type 相同。

所以我对 Stack Overflow 的问题:

    为什么旧编译器不会像新编译器那样破坏插入的变量?我的意思是,即使 GCC 4.7 也没有这样做,而且它非常符合标准。

    这个问题是否广为人知,因为升级编译器肯定会导致以前可以工作的代码突然停止工作?

    C++ 标准委员会是否有意采取这种行为?

    您如何建议修改 unordered_map::insert() 以提供更好的行为?我问这个是因为如果这里有支持,我打算将此行为作为 N 注释提交给 WG21,并要求他们实施更好的行为。

【问题讨论】:

仅仅因为它使用通用 ref 并不意味着插入的值总是被移动 - 它应该只对右值这样做,而普通的 a 不是。它应该制作一个副本。此外,这种行为完全取决于标准库,而不是编译器。 这似乎是库实现中的一个错误 “因此,根据 C++11 标准,插入破坏行为实际上是正确的,而旧的编译器是不正确的。”对不起,但你错了。您是从 C++ 标准的哪个部分得到这个想法的? BTW cplusplus.com 不是官方的。 我无法在我的系统上重现这个,我分别使用 gcc 4.8.2 和 4.9.0 20131223 (experimental)。对我来说,输出是a.second is now 0x2074088 (或类似的)。 This was GCC bug 57619,4.8 系列中的回归,在 2013-06 中针对 4.8.2 进行了修复。 【参考方案1】:

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

您观察到的行为总是移动,是 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

【讨论】:

Clang 默认使用 libstdc++,GCC 的 stdlib。 我正在使用 gcc 4.9,我正在查看 libstdc++-v3/include/bits/。我没有看到同样的事情。我看到 return _M_h.insert(std::forward&lt;_Pair&gt;(__x)); 。 4.8 可能会有所不同,但我还没有检查过。 是的,所以我猜他们修复了这个错误。 @Brian 不,我刚刚检查了我的系统标头。一样。 4.8.2 顺便说一句。 我的是 4.8.1,所以我猜它在两者之间是固定的。【参考方案2】:
template <class P> pair<iterator,bool> insert ( P&& val );

这是一个贪婪的通用引用移动重载,消耗与任何其他重载不匹配的任何内容,并将其构造为 value_type。

这就是一些人所说的通用引用,但实际上是引用折叠。在您的情况下,如果参数是 pair&lt;int,shared_ptr&lt;int&gt;&gt; 类型的 lvalue,它将 导致参数是右值引用并且它 不应该 离开它。

那么为什么我们上面的代码选择了这个重载,而不是 unordered_map::value_type 可能像大多数人所期望的那样重载?

因为您和之前的许多其他人一样,误解了容器中的value_type*mapvalue_type(无论是有序还是无序)是 pair&lt;const K, T&gt;,在您的情况下是 pair&lt;const int, shared_ptr&lt;int&gt;&gt;。不匹配的类型消除了您可能期望的重载:

iterator       insert(const_iterator hint, const value_type& obj);

【讨论】:

我仍然不明白为什么这个新的引理存在,“通用引用”,并不真正意味着任何特定的东西,也没有为一个完全不是“通用”的东西提供一个好名字在实践中。最好记住一些旧的折叠规则,这些规则是 C++ 元语言行为的一部分,因为在语言中引入了模板,再加上 C++11 标准的新签名。无论如何,谈论这种通用参考有什么好处?已经有足够奇怪的名字和东西了,比如 std::move 根本不会移动任何东西。 @user2485710 有时,无知是幸福,并且在所有参考折叠和模板类型推导调整中使用总称“通用参考”,恕我直言,这很不直观,加上使用std::forward 的要求那个调整做真正的工作......斯科特迈耶斯做得很好,为转发设置了非常简单的规则(使用通用引用)。 在我看来,“通用引用”是一种用于声明可以绑定到左值和右值的函数模​​板参数的模式。 “引用折叠”是在“通用引用”情况和其他上下文中将(推导或指定)模板参数替换为定义时发生的情况。 @aschepler:通用参考只是参考折叠子集的一个花哨的名称。我同意无知是幸福,而且拥有一个花哨的名字会使谈论它更容易和更时髦,这可能有助于传播这种行为。话虽如此,我并不是这个名字的忠实拥护者,因为它将实际规则推到了一个角落,在那里它们不需要知道......也就是说,直到你在外面遇到一个案例Scott Meyers 所描述的。 现在语义毫无意义,但我试图提出的区别是:通用引用发生在我设计并将函数参数声明为 deducible-template-parameter &amp;&amp; 时;当编译器实例化模板时会发生引用折叠。引用折叠是通用引用起作用的原因,但我的大脑不喜欢将这两个术语放在同一个域中。

以上是关于C++ 标准委员会是不是打算在 C++11 中 unordered_map 破坏它插入的内容?的主要内容,如果未能解决你的问题,请参考以下文章

C++程序员必备好书:《C++并发编程实战》(第2版)出版啦

祝贺龙蜥社区理事单位成员入选C++ 标准委员会,首个国内企业代表加入

C++ hexfloat 编译时解析

Concept check

C++14介绍

C++0x草案将于年内发表,C++即将重大升级