关于gcc:C ++标准委员会是否打算在C ++ 11中使用unordered_map来破坏它所插入的内容?

Is it intended by the C++ standards committee that in C++11 unordered_map destroys what it inserts?

我刚刚花了三天的时间来追踪一个非常奇怪的bug,在这个bug中无序的map::insert()会破坏您插入的变量。这种非常不明显的行为只出现在最近的编译器中:我发现clang 3.2-3.4和gcc 4.8是唯一演示这种"特性"的编译器。

下面是我的主代码库中的一些简化代码,它演示了这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
#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++程序员一样,期望输出看起来像这样:

1
2
a.second is 0x8c14048
a.second is now 0x8c14048

但是有了clang 3.2-3.4和gcc 4.8,我得到了:

1
2
a.second is 0xe03088
a.second is now 0

这可能毫无意义,除非您仔细检查文档中是否存在无序的_map::insert(),网址为http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/,其中重载2是:

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

这是一个贪婪的通用引用move重载,使用任何与其他重载不匹配的内容,并将其构造为value_类型。那么,为什么上面的代码选择了这个重载,而不是像大多数人预期的那样无序的_map::value_类型重载?

答案直视你的脸:无序的值类型是一对编译器会正确地认为一对是不可转换的。因此,编译器选择move universal引用重载,这会破坏原始的引用重载,尽管程序员没有使用std::move()这是一个典型的约定,用于指示您对一个变量是否可以被破坏。因此,插入破坏行为实际上是按照C++ 11标准纠正的,而旧编译器是错误的。

你现在可能知道为什么我花了三天时间诊断这个错误了。在一个大型代码库中,插入无序映射的类型是一个远在源代码术语中定义的typedef,这一点一点也不明显,而且任何人都没有想到检查typedef是否与值类型相同。

所以我的问题是堆栈溢出:

  • 为什么旧的编译器不会像新的编译器那样破坏插入的变量?我的意思是,即使GCC4.7也不能做到这一点,而且它相当符合标准。

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

  • C++标准委员会打算实施这种行为吗?

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


  • 正如其他人在评论中指出的那样,"通用"构造函数实际上并不应该总是从它的参数开始移动。如果参数真的是右值,它应该移动,如果是左值,它应该复制。

    你观察到的行为,总是在移动,是libstdc++中的一个bug,根据对这个问题的评论,这个bug现在被修复了。对于那些好奇的人,我看了一下G++-4.8的头。

    bits/stl_map.h,598-603行

    1
    2
    3
    4
    5
    6
      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号线

    1
    2
    3
    4
    5
    6
      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


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

    Which is a greedy universal reference move overload, consuming anything not matching any of the other overloads, and move constructing it into a value_type.

    这就是一些人所说的普遍参考,但实际上是参考崩溃。在您的情况下,如果参数是pair>类型的左值,则不会导致该参数是右值引用,并且不应从中移动。

    So why did our code above choose this overload, and not the
    unordered_map::value_type overload as probably most would expect?

    因为你和以前的许多人一样,误解了容器中的value_type*mapvalue_typepair,您的情况是pair>。不匹配的类型消除了您可能期望的重载:

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