C ++标准委员会打算在C ++ 11中unordered_map销毁它插入的内容吗?


114

我刚刚度过了三天的时间,一直在寻找一个非常奇怪的错误,在这个错误中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是pair < const int,std :: shared_ptr>,编译器会正确地认为pair < int,std :: shared_ptr>是不可转换的。因此,尽管程序员没有使用std :: move(),编译器选择了移动通用引用重载,并且破坏了原始重载,这是典型的约定,用于表明您可以使用被破坏的变量。因此,按照C ++ 11标准,插入破坏行为实际上是正确的,而较早的编译器是错误的

您现在可能知道为什么我花了三天的时间来诊断此错误。在大型代码库中,这一点都不明显,因为插入源代码术语中定义的typedef是插入unordered_map中的类型,并且从未有人检查过typedef是否与value_type相同。

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

  1. 为什么较旧的编译器不会破坏像较新的编译器那样插入的变量?我的意思是,即使GCC 4.7也没有做到这一点,并且符合标准。

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

  3. C ++标准委员会是否打算这样做?

  4. 您如何建议修改unordered_map :: insert()以获得更好的行为?我之所以这样问是因为,如果这里有支持,我打算将此行为作为N笔记提交给WG21,并请他们实施更好的行为。


10
仅仅因为它使用通用ref并不意味着插入的值总是会移动-它只应该对rvalues进行移动,而平原则a不会。它应该制作一个副本。同样,此行为完全取决于stdlib,而不取决于编译器。
Xeo 2014年

10
这似乎是在库的实现中的错误
大卫·罗德里格斯- dribeas

4
“因此,按照C ++ 11标准,插入破坏行为实际上是正确的,而较早的编译器是不正确的。” 抱歉,您错了。您是从C ++标准的哪个部分得到这个想法的?BTW cplusplus.com不是官方的。
Ben Voigt 2014年

1
我无法在系统上重现此代码,并且4.9.0 20131223 (experimental)分别使用gcc 4.8.2和。输出a.second is now 0x2074088 对我来说(或类似)。

47
这是GCC错误57619这是 4.8系列的回归,该系列已在2013-06年修复为4.8.2。
Casey 2014年

Answers:


83

正如其他人在评论中指出的那样,实际上,“通用”构造函数并不总是偏离其参数。如果参数确实是右值,则应该移动;如果是左值,则应复制。

您观察到的行为一直在变化,这是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


11
Clang默认使用libstdc ++,即GCC的stdlib。
Xeo 2014年

我正在使用gcc 4.9,正在寻找libstdc++-v3/include/bits/。我看不到同一件事。我知道了{ return _M_h.insert(std::forward<_Pair>(__x)); }。4.8可能会有所不同,但我尚未检查。

是的,所以我想他们已经修复了该错误。
布莱恩

@Brian Nope,我刚刚检查了系统标题。一样。4.8.2 btw。

我的4.8.1,所以我猜它是固定的。
布赖恩

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

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

这就是某些人所说的通用参考,但实际上是参考崩溃了。在您的情况下,如果参数是类型的左值pair<int,shared_ptr<int>>不会导致参数是右值引用,并且不应从中移出。

那么,为什么我们上面的代码选择了这种重载,而不是大多数人期望的unordered_map :: value_type重载呢?

因为您和以前的许多人一样会误解value_type容器中的。在value_type*map(无论是有序还是无序一)是pair<const K, T>,它在你的情况pair<const int, shared_ptr<int>>。不匹配的类型消除了您可能期望的重载:

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

我仍然不明白为什么存在这个新的引理,即“通用参考”,实际上并不意味着任何具体的含义,也没有为实际上完全不是“通用”的事物起好名字。最好记住一些旧的折叠规则,这些规则是C ++元语言行为的一部分,因为自从模板是用该语言引入以来,就加上了C ++ 11标准的新签名。无论如何,谈论这个通用引用有什么好处?已经有一些名字和东西很奇怪了,像std::move那样根本不会动任何东西。
user2485710 2014年

2
@ user2485710有时,无知是一种幸福,并且在所有引用折叠和模板类型推导调整上使用总括性术语“通用参考”,恕我直言,这很不直观,并且要求使用std::forward该调整来完成实际工作... Scott Meyers在设置非常简单的转发规则(使用通用引用)方面做得很好。
Mark Garcia

1
在我看来,“通用引用”是一种声明可以绑定到左值和右值的函数模​​板参数的模式。“引用折叠”是在“通用引用”情况和其他上下文中将模板参数替换(推导或指定)模板参数时所发生的情况。
aschepler 2014年

2
@aschepler:通用引用只是引用折叠子集的一个花哨名称。我同意这种无知是幸福,还有一个事实,那就是花哨的名字使谈论它变得容易和时尚,并且可能有助于传播这种行为。话虽这么说,我不是这个名字的忠实拥护者,因为它将实际规则推到了一个不需要知道它们的角落……也就是说,直到您遇到了Scott Meyers所描述的情况之外的情况。
DavidRodríguez-dribeas 2014年

现在没有意义的语义,但是我试图暗示的区别是:我在设计并将函数参数声明为deducible-template-parameter时发生了通用引用&&;当编译器实例化模板时,引用崩溃。引用折叠是通用引用起作用的原因,但我的大脑不喜欢将两个术语放在同一域中。
aschepler 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.