std :: map \ templates /插入移动值

时间:2014-06-11 06:49:47

标签: c++ map c++14

目前我正在阅读C ++的1篇论文,目前我正在尝试理解题为Improved insertion interface for unique-key maps的n3873论文。本文指出insert和emplace方法存在问题,它通过以下示例说明了问题:

std::map<std::string, std::unique_ptr<Foo>> m;
m["foo"];

std::unique_ptr<Foo> p(new Foo);
auto res = m.emplace("foo", std::move(p));

在上面的代码之后,它表达了以下内容:

  

p的价值是多少?目前尚未说明p是否已被移出。 (答案是它取决于库的实现。)

好吧,我在寻找前一个引文的解释时遇到了麻烦,主要是因为我无法找到标准中指定的位置,如上所述移动或不移动{{1是实现定义;查看n3690 standard关联容器部分(23.2.4)关于p(插入使用emplace(args)构建的value_type对象t)和{{1} } methods仅提到插入或放置了值...

  

...当且仅当容器中没有元素且密钥等同于std::forward<Args>(args)的键时。

关于移动(或不移动)insert(t)值的说法;另一方面,t托管内存无论如何都被释放(如果移动t在无插入后被释放,并且如果未被移动则在范围结束时被释放)isn不是吗?


介绍之后,让我问下列问题:

  • 为什么在将值插入/放入已经具有插入键的关联容器中时移动值,将值设置为未指定状态?
  • 措辞在哪里,这个操作是实现定义的?
  • 示例的p会发生什么?它真的被释放了吗?

如果问题看起来很愚蠢或有明显的答案,请尝试原谅,这可能是因为我缺乏英语理解能力,或者因为我不习惯潜入标准论文。任何指导都将不胜感激。

4 个答案:

答案 0 :(得分:11)

  

关于移动(或不移动)

这正是问题,在mapped_type将被移动的条件下,未指定。

  

为什么在将值插入/移入已经具有插入键的关联容器中时移动值,将值设置为未指定状态?

没有什么可以阻止实现首先将unique_ptr移动到临时变量中,然后搜索密钥"foo"。在这种情况下,无论map是否已包含密钥,p == nullptr都会在emplace的调用返回时显示。{/ p>

相反,实现可以有条件地移动,具体取决于密钥是否存在。然后,如果密钥存在,则在函数调用返回时p != nullptr。两种方法都同样正确,并且在第一种情况下,即使插入永远不会发生,也无法检索p的原始内容,它将在emplace返回时被销毁

建议的emplace_stable()emplace_or_update()函数可以在所有情况下使行为可预测。

  

措辞在哪里,这个操作是实现定义的?

它没有被指定为实现定义,它在指定的范围内,允许实现过多的自由度,可能导致行为并不总是令人满意。

  

示例的p会发生什么?它真的被释放了吗?

在您已展示的示例中,p的内容不会插入到地图中(因为密钥"foo"已经存在)。但p可能会或可能不会从emplace的调用返回时移动。

在任何情况下都不会有资源泄漏。如果实现无条件地移动p,它会将其移动到本地副本中,如果密钥存在则将其销毁,或者如果密钥不存在则插入到映射中。

另一方面,如果实施有条件地移动p,它将被插入map,或者p将在emplace时拥有它回报。在后一种情况下,当p超出范围时,它会被销毁。

答案 1 :(得分:4)

c ++中的移动语义与emplace / insert方法无关。后者只是使用移动语义来获得性能的案例之一。

你应该学习rvalue引用并移动语义,以便理解为什么p在行“m.emplace(”foo“,std :: move(p))之后有未定义的值;”

您可以在此处详细阅读: http://www.slideshare.net/oliora/hot-c11-1-rvalue-references-and-move-semantics

简而言之,std :: move(p)语句告诉编译器你不再关心p的内容了,并且完全可以将它们移动到其他地方。实际上,std :: move(p)将p转换为右值参考类型(T&amp;&amp;)。 rvalue存在于c ++之前的c ++ 11中而没有“官方”类型。例如,expression(string(“foo”)+ string(“bar”))生成rvalue,它是一个包含“foobar”的分配缓冲区的字符串。在c ++ 11之前,你不能使用这个表达式是完全临时的,并且会在一秒钟内消失(除了在编译器优化中)。现在,您将此作为语言的一部分:

v.emplace_back(string("foo") + string("bar"))

将获取临时字符串并将其内容直接移动到容器中(无冗余分配)。

它可以优雅地使用临时表达式,但不能直接使用变量(与rvalues相反)。但是,在某些情况下,您知道您不再需要此变量,而您希望将其移动。为此,您使用std :: move(..)告诉编译器将此变量视为右值。您需要了解之后不能使用它。这是你和编译器之间的契约。

答案 2 :(得分:2)

我认为17.6.4.9/1 [res.on.arguments]的第三个子弹适用于此处(引用N3936):

  

以下各项适用于C ++标准库中定义的函数的所有参数,除非另有明确说明。

     
      
  • 如果函数的参数具有无效值(例如函数域外的值或指针对其预期用途无效),则行为未定义。
  •   
  • 如果一个函数参数被描述为一个数组,那么实际传递给该函数的指针应该具有一个值,使得所有地址计算和对象的访问(如果指针确实指向这个对象的第一个元素,那么它将是有效的一个数组)实际上是有效的。
  •   
  • 如果函数参数绑定到右值引用参数,则实现可以假设   此参数是对此参数的唯一引用。 [注意:如果参数是T&&形式的通用参数并且绑定了类型A的左值,则参数绑定到左值引用(14.8.2.1),因此不包括上一句。 -end note] [注意:如果程序在将左值传递给库函数时将左值转换为x值(例如通过调用带有参数move(x)的函数),程序实际上是要求该函数来处理左值作为临时值。实现可以自由地优化别名检查,如果参数是左值,则可能需要这些检查。 - 尾注]
  •   

通过将引用对象的rvalue表达式传递给引用参数,您实际上是给予标准库权限,以便对该对象执行任何操作。它可以从对象移动,也可以不移动,或者以便于标准库实现的任何其他方式对其进行修改。

答案 3 :(得分:2)

与链接文章所说的相反,我会说标准的语言几乎可以保证这段代码做错了:它将指针从p移开,然后销毁p最初指向的对象{1}}因为最终没有任何内容插入地图m(因为从"foo"构造的密钥已经存在)。 [我说&#34;几乎&#34;只是因为标准的语言不如人们所希望的那么明确;显然,手头的问题根本就不在于谁写过这个问题。]

引用23.2.4中的表102,条目a_uniq.emplace(args),效果为

  

插入value_type t对象构建std::forward<Args>(args)...当且仅当容器中没有元素且密钥等效于t的键时。

value_type案例std::map std::pair<const Key, T> Keystd::string等于Tstd::unique_ptr<Foo>等于t std::pair<const std::string, std::unique_ptr<Foo>> t("foo", std::move(p)); 。所以提到的对象t是(或将会)构造为

t

t&#34;的关键字是这对中的第一个组成部分。正如链接文章所指出的那样,由于“构造”和“插入”的混合,语言是不精确的:人们可能会解释说“当且仅当&#34;引用它们两者,因此p 既不构造也不插入,以防容器中的元素具有等同于p的键的键;然后在这种情况下,不会从t移动任何东西(因为缺少构造),t不会变为空。但是,在对所引用的短语的阅读中存在逻辑上的不一致:如果永远不应该构建t,那么t&#34;的关键是什么?参考?因此,我认为对该文本的唯一合理解读是:对象t是(无条件地)按照指示构造的,然后t被插入到容器中,当且仅当容器中没有元素时密钥等同于emplace的密钥。在未插入t的情况下(如示例中所示),临时将在从t的调用返回时消失,从而销毁移入其中的资源。

当然这并不意味着实现不可能做正确的事情:单独构造p的第一个(关键)组件,在容器中查找该键和只有在找不到时构建完整的对t(此时将映射到对象的表单t移动到emplace的第二个组件)并插入。 (这确实要求密钥类型是复制或移动可构造的,因为将成为auto p = m.emplace(key,std::move(mapped_to_value)); if (not p.second) // no insertion took place { /* some action with value p.first->second about to be overwritten here */ p.first->second = std::move(mapped_to_value) // replace mapped-to value } 的第一个组件最初是在不同的地方构建的。)正是因为这样的实现可能是文章建议提供的一种可靠地要求这种行为的手段。但是,该标准的当前语言似乎并未授予此类实施许可,甚至没有义务采取这样的行为。


让我补充一点,我在实践中遇到了这个问题,因为我天真地认为拥有一个很好的新方法auto range = m.equal_range(key); if (range.first==range.second) // the key was previously absent; insert a pair m.emplace_hint(range.first,key,std::move(mapped_to_value)); else // the key was present, replace the associated value { /* some action with value range.first->second about to be overwritten here */ range.first->second = std::move(mapped_to_value) // replace mapped-to value } ,它肯定会被定义为与移动语义一起使用。所以我写了一些东西:

unordered_map

事实证明并非如此,并且在我的&#34;映射到&#34;类型,恰好包含共享指针和唯一指针,共享指针组件表现良好,但如果映射中的前一个条目被覆盖,则唯一指针组件将变为null。鉴于这个成语不起作用,我把它改写为

emplace_hint

这是一个合理的解决方法,对映射类型没有太多假设(特别是它不需要是默认构造或可复制构造,只需移动构造和移动可分配)。

看起来这个成语甚至应该适用于std::map,尽管我没有尝试过这种情况。事实上,仔细观察是有效的,但使用std::unordered_map::equal_range毫无意义,因为与std::unordered_map::end的情况不同,方法std::unordered_map::emplace_hint 有义务以防万一返回一对迭代器的缺席键,它们等于emplace_hint返回的(无信息)值,而不是其他一对等迭代器。事实上似乎允许忽略提示的t几乎被迫这样做,因为密钥已经存在且m.find应该什么都不做(除了吞噬可能进入的资源)它的临时对m.equal_range),否则(没有这样的键存在)没有办法获得有用的提示,因为方法m.end()和{{1}}都不允许返回任何其他内容当用一个原来缺席的密钥调用时,{{1}}。{/ p>