在 C++ 地图中插入 vs emplace vs operator[]

Posted

技术标签:

【中文标题】在 C++ 地图中插入 vs emplace vs operator[]【英文标题】:insert vs emplace vs operator[] in c++ map 【发布时间】:2013-06-14 20:33:36 【问题描述】:

我是第一次使用地图,我意识到插入元素的方法有很多种。您可以使用emplace()operator[]insert(),以及使用value_typemake_pair 等变体。虽然有很多关于所有这些的信息和关于特定案例的问题,但我仍然无法理解大局。 所以,我的两个问题是:

    他们每个人的优势是什么?

    是否需要在标准中添加 emplace?没有它,有什么是以前做不到的吗?

【问题讨论】:

位置语义允许显式转换和直接初始化。 现在operator[] 基于try_emplaceinsert_or_assign 也值得一提。 @FrankHB 如果您(或其他人)添加了最新的答案,我可以更改已接受的答案。 【参考方案1】:

Emplace:利用右值引用来使用您已经创建的实际对象。这意味着不调用复制或移动构造函数,这对 LARGE 对象有好处! O(log(N)) 时间。

插入:具有标准左值引用和右值引用的重载,以及要插入的元素列表的迭代器,以及关于元素所属位置的“提示”。使用“提示”迭代器可以将插入时间缩短到恒定时间,否则为 O(log(N)) 时间。

Operator[]:检查对象是否存在,如果存在,则修改对该对象的引用,否则使用提供的键和值对两个对象调用make_pair,然后做与插入功能。这是 O(log(N)) 时间。

make_pair:只做一对。

没有“需要”将 emplace 添加到标准中。在 c++11 中,我相信添加了 && 类型的引用。这消除了移动语义的必要性,并允许优化某些特定类型的内存管理。特别是右值引用。重载的 insert(value_type &&) 运算符没有利用 in_place 语义,因此效率低得多。虽然它提供了处理右值引用的能力,但它忽略了它们的关键目的,即就地构造对象。

【讨论】:

没有“必要”将 emplace 添加到标准中。” 这显然是错误的。 emplace() 只是插入无法复制或移动的元素的唯一方法。 (是的,也许,最有效地插入一个复制和移动构造函数比构造成本高得多的构造函数,如果存在这样的事情)看起来你的想法也错了:这不是关于“[利用]使用您已经创建的实际对象的右值引用";还没有创建对象,并且您转发 map 参数 it 需要在其内部创建它。你没有制造物体。 @underscore_d 已在编辑中修复。 @Ben_LCDB 感谢您抽出宝贵时间尝试!但我不同意大幅改变帖子含义的编辑。如果作者想修复他们的帖子,他们可以。我不认为这是其他成员通过改变情绪来为他们“修复”它的地方。否则没有人有时间发布好的答案,因为他们会用它来“修复”所有不好的答案...... 是不是该部分的顺序变化让您认为它是实质性的?没关系!【参考方案2】:

除了优化机会和更简单的语法之外,插入和就位之间的一个重要区别是后者允许显式转换。 (这适用于整个标准库,而不仅仅是地图。)

这里有一个例子来演示:

#include <vector>

struct foo

    explicit foo(int);
;

int main()

    std::vector<foo> v;

    v.emplace(v.end(), 10);      // Works
    //v.insert(v.end(), 10);     // Error, not explicit
    v.insert(v.end(), foo(10));  // Also works

诚然,这是一个非常具体的细节,但是当您处理用户定义的转换链时,请牢记这一点。

【讨论】:

想象一下 foo 在它的 ctor 中需要两个 int 而不是一个。你能用这个电话吗? v.emplace(v.end(), 10, 10); ...或者您现在需要使用:v.emplace(v.end(), foo(10, 10) ); 吗? 我现在无法使用编译器,但我认为这意味着这两个版本都可以工作。您看到的emplace 的几乎所有示例都使用了一个接受单个参数的类。如果在示例中使用多个参数,IMO 实际上会使 emplace 的可变参数语法的性质更加清晰。【参考方案3】:

在地图的特定情况下,旧选项只有两个:operator[]insertinsert 的不同风格)。所以我将开始解释这些。

operator[] 是一个查找或添加运算符。它将尝试在地图中查找具有给定键的元素,如果存在,它将返回对存储值的引用。如果没有,它将使用默认初始化创建一个插入到位的新元素并返回对它的引用。

insert 函数(在单元素风格中)采用 value_typestd::pair&lt;const Key,Value&gt;),它使用键(first 成员)并尝试插入它。因为std::map 不允许重复,如果存在现有元素,它不会插入任何内容。

两者的第一个区别是operator[]需要能够构造一个默认初始化的,因此不能用于不能默认初始化的值类型。两者之间的第二个区别是当已经存在具有给定键的元素时会发生什么。 insert 函数不会修改映射的状态,而是返回一个指向元素的迭代器(以及一个 false 表示它没有被插入)。

// assume m is std::map<int,int> already has an element with key 5 and value 0
m[5] = 10;                      // postcondition: m[5] == 10
m.insert(std::make_pair(5,15)); // m[5] is still 10

insert 的情况下,参数是value_type 的对象,可以以不同的方式创建。您可以使用适当的类型直接构造它或传递可以构造 value_type 的任何对象,这就是 std::make_pair 发挥作用的地方,因为它允许简单地创建 std::pair 对象,尽管它可能不是你想要什么...

以下调用的净效果是相似的

K t; V u;
std::map<K,V> m;           // std::map<K,V>::value_type is std::pair<const K,V>

m.insert( std::pair<const K,V>(t,u) );      // 1
m.insert( std::map<K,V>::value_type(t,u) ); // 2
m.insert( std::make_pair(t,u) );            // 3

但实际上并不相同... [1] 和 [2] 实际上是等价的。在这两种情况下,代码都会创建一个相同类型的临时对象 (std::pair&lt;const K,V&gt;) 并将其传递给 insert 函数。 insert 函数将在二叉搜索树中创建适当的节点,然后将 value_type 部分从参数复制到节点。使用value_type 的优势在于,value_type 总是匹配 value_type,您不能错误输入std::pair 参数的类型!

区别在于 [3]。函数std::make_pair 是一个模板函数,它将创建一个std::pair。签名是:

template <typename T, typename U>
std::pair<T,U> make_pair(T const & t, U const & u );

我故意不向std::make_pair 提供模板参数,因为这是常见用法。这意味着模板参数是从调用中推导出来的,在本例中为T==K,U==V,因此对std::make_pair 的调用将返回std::pair&lt;K,V&gt;(注意缺少的const)。签名需要value_type,即close,但与调用std::make_pair 的返回值不同。因为它足够接近,它将创建一个正确类型的临时文件并复制初始化它。这将依次复制到节点,总共创建两个副本。

这可以通过提供模板参数来解决:

m.insert( std::make_pair<const K,V>(t,u) );  // 4

但这仍然容易出错,就像在 case [1] 中显式键入类型一样。

到目前为止,我们有不同的调用 insert 的方法,需要在外部创建 value_type 并将该对象的副本复制到容器中。或者,如果类型是 defaultconstructibleassignable(特意只关注m[k]=v),您可以使用operator[],并且它需要一个对象的默认初始化和将值复制到该对象中。

在 C++11 中,通过可变参数模板和完美转发,有一种新方法可以通过 emplaceing(就地创建)将元素添加到容器中。不同容器中的 emplace 函数基本上做同样的事情:该函数不是获取一个 source 并从中 copy 到容器中,而是采用将要转发给存储在容器中的对象的构造函数。

m.emplace(t,u);               // 5

在 [5] 中,std::pair&lt;const K, V&gt; 没有创建并传递给emplace,而是对tu 对象的引用被传递给emplace,后者将它们转发给@ 的构造函数987654371@ 数据结构内的子对象。在这种情况下,std::pair&lt;const K,V&gt;no 副本完全完成,这是 emplace 优于 C++03 替代方案的优势。与insert 的情况一样,它不会覆盖地图中的值。


一个我没有想到的有趣问题是emplace 可以如何实际用于地图,这在一般情况下不是一个简单的问题。

【讨论】:

答案中暗示了这一点,但如果存在, map[]=val 将覆盖以前的值。 在我看来,一个更有趣的问题是它没有什么用处。因为您保存了对副本,这很好,因为没有对副本意味着没有mapped_type 不是副本。我们想要的是在对中放置mapped_type 的构造,并在映射中放置对构造。因此,std::pair::emplace 函数及其在map::emplace 中的转发支持都缺失了。在其当前形式中,您仍然必须将构造的 mapped_type 提供给将复制它的 pair 构造函数一次。比两倍好,但还是不行。 实际上我修改了该注释,在 C++11 中有一个模板对构造函数,其作用与 emplace 在 1 个参数构造的情况下完全相同。还有一些奇怪的分段构造,正如他们所说,使用元组来转发参数,所以我们仍然可以有完美的转发。 在 unordered_map 和 map 中似乎存在 insert 的性能错误:link insert_or_assigntry_emplace(都来自C++17)的信息更新这个可能会很好,这有助于填补现有方法的一些功能空白。【参考方案4】:

以下代码可以帮助您理解insert()emplace() 的“大局观”。

代码摘要:Foo 类使用static int foo_counter 跟踪迄今为止已构造/移动的Foo 对象的总数。每个Foo 对象还将foo_counter 的值(在其创建时)存储在局部变量int val; 中,如果val8,那么Foo 对象将被称为“foo8”或“Foo 8”等。每次调用Foo 构造函数时,它都会输出有关对stdout 的调用的信息(例如,调用Foo(11) 将输出“Foo(int) with val: 11”)。 main() 中的代码打印到stdout 将执行的语句(例如umap.emplace(11, d))然后执行它。

#include <iostream>
#include <unordered_map>
#include <utility>

//Foo simply outputs what constructor is called with what value.
struct Foo 
  static int foo_counter; //Track how many Foo objects have been created.
  int val; //This Foo object was the val-th Foo object to be created.

  Foo()  val = foo_counter++;
    std::cout << "Foo() with val:                " << val << '\n';
  
  Foo(int value) : val(value)  foo_counter++;
    std::cout << "Foo(int) with val:             " << val << '\n';
  
  Foo(Foo& f2)  val = foo_counter++;
    std::cout << "Foo(Foo &) with val:           " << val
              << " \tcreated from:      \t" << f2.val << '\n';
  
  Foo(const Foo& f2)  val = foo_counter++;
    std::cout << "Foo(const Foo &) with val:     " << val
              << " \tcreated from:      \t" << f2.val << '\n';
  
  Foo(Foo&& f2)  val = foo_counter++;
    std::cout << "Foo(Foo&&) moving:             " << f2.val
              << " \tand changing it to:\t" << val << '\n';
  
  ~Foo()  std::cout << "~Foo() destroying:             " << val << '\n'; 

  Foo& operator=(const Foo& rhs) 
    std::cout << "Foo& operator=(const Foo& rhs) with rhs.val: " << rhs.val
              << " \tcalled with lhs.val = \t" << val
              << " \tChanging lhs.val to: \t" << rhs.val << '\n';
    val = rhs.val;
    return *this;
  

  bool operator==(const Foo &rhs) const  return val == rhs.val; 
  bool operator<(const Foo &rhs)  const  return val < rhs.val;  
;

int Foo::foo_counter = 0;

//Create a hash function for Foo in order to use Foo with unordered_map
namespace std 
   template<> struct hash<Foo> 
       std::size_t operator()(const Foo &f) const 
           return std::hash<int>(f.val);
       
   ;


int main()

    std::unordered_map<Foo, int> umap;
    int d; //Some int that will be umap's value. It is not important.

    //Print the statement to be executed and then execute it.

    std::cout << "\nFoo foo0, foo1, foo2, foo3;\n";
    Foo foo0, foo1, foo2, foo3;

    std::cout << "\numap.insert(std::pair<Foo, int>(foo0, d))\n";
    umap.insert(std::pair<Foo, int>(foo0, d));
    //Side note: equiv. to: umap.insert(std::make_pair(foo0, d));

    std::cout << "\numap.insert(std::move(std::pair<Foo, int>(foo1, d)))\n";
    umap.insert(std::move(std::pair<Foo, int>(foo1, d)));
    //Side note: equiv. to: umap.insert(std::make_pair(foo1, d));
    
    std::cout << "\nstd::pair<Foo, int> pair(foo2, d)\n";
    std::pair<Foo, int> pair(foo2, d);

    std::cout << "\numap.insert(pair)\n";
    umap.insert(pair);

    std::cout << "\numap.emplace(foo3, d)\n";
    umap.emplace(foo3, d);
    
    std::cout << "\numap.emplace(11, d)\n";
    umap.emplace(11, d);

    std::cout << "\numap.insert(12, d)\n";
    umap.insert(12, d);

    std::cout.flush();

我得到的输出是:

Foo foo0, foo1, foo2, foo3;
Foo() with val:                0
Foo() with val:                1
Foo() with val:                2
Foo() with val:                3

umap.insert(std::pair<Foo, int>(foo0, d))
Foo(Foo &) with val:           4    created from:       0
Foo(Foo&&) moving:             4    and changing it to: 5
~Foo() destroying:             4

umap.insert(std::move(std::pair<Foo, int>(foo1, d)))
Foo(Foo &) with val:           6    created from:       1
Foo(Foo&&) moving:             6    and changing it to: 7
~Foo() destroying:             6

std::pair<Foo, int> pair(foo2, d)
Foo(Foo &) with val:           8    created from:       2

umap.insert(pair)
Foo(const Foo &) with val:     9    created from:       8

umap.emplace(foo3, d)
Foo(Foo &) with val:           10   created from:       3

umap.emplace(11, d)
Foo(int) with val:             11

umap.insert(12, d)
Foo(int) with val:             12
Foo(const Foo &) with val:     13   created from:       12
~Foo() destroying:             12

~Foo() destroying:             8
~Foo() destroying:             3
~Foo() destroying:             2
~Foo() destroying:             1
~Foo() destroying:             0
~Foo() destroying:             13
~Foo() destroying:             11
~Foo() destroying:             5
~Foo() destroying:             10
~Foo() destroying:             7
~Foo() destroying:             9

这段代码及其输出显示了insert()emplace() 之间的主要“大图”区别是:

而使用insert() 几乎总是需要在main() 的范围内构造或预先存在一些Foo 对象(后跟复制或移动),如果使用@987654351 @ 然后对Foo 构造函数的任何调用完全在unordered_map 内部完成(即在emplace() 方法定义的范围内)。传递给emplace() 的键的参数直接转发到Foo 定义中的Foo 构造函数调用(可选的附加详细信息:此新构造的对象立即并入@987654358 之一@ 的成员变量,这样当执行离开 emplace() 时不会调用析构函数,也不会调用移动或复制构造函数。

注意:上述“几乎总是”中出现“几乎”的原因是因为insert()的一个重载实际上相当于emplace()。如in this cppreference.com page 所述,重载template&lt;class P&gt; std::pair&lt;iterator, bool&gt; insert(P&amp;&amp; value)(即此cppreference.com 页面上insert() 的重载(2))等效于emplace(std::forward&lt;P&gt;(value))。我不会再讨论这个特定的技术性问题。

我现在将详细介绍代码及其输出。

    首先,请注意unordered_map 始终在内部存储Foo 对象(而不是Foo *s)作为键,当unordered_map 被销毁时,这些对象都将被销毁。在这里,unordered_map 的内部键是 foos 13、11、5、10、7 和 9。
所以从技术上讲,我们的unordered_map 实际上存储了std::pair&lt;const Foo, int&gt; 对象,而后者又存储了Foo 对象。但是要了解emplace()insert() 的不同之处的“大局观”(参见上面突出显示的框),可以暂时将这个std::pair 对象想象为完全被动的。一旦您理解了这个“大局观”,重要的是要备份并了解unordered_map 对这个中介std::pair 对象的使用如何引入了微妙但重要的技术性。

    insert()foo0foo1foo2 中的每一个都需要 2 次调用 Foo 的复制/移动构造函数之一和 2 次调用 Foo 的析构函数(就像我现在描述):

    insert()ing 每个foo0foo1 创建了一个临时对象(分别为foo4foo6),然后在插入完成后立即调用其析构函数。此外,unordered_map 的内部Foos(foos 5 和 7)也调用了它们的析构函数,当 unordered_map 在执行到 main() 结束时被销毁。 为了insert()foo2,我们首先显式创建了一个非临时pair对象(称为pair),它在foo2上调用Foo的复制构造函数(创建foo8作为内部pair 的成员)。然后我们insert()ed 这对,导致unordered_map 再次调用复制构造函数(在foo8 上)以创建自己的内部副本(foo9)。与 foos 0 和 1 一样,最终结果是对这个 insert()ion 的两次析构函数调用,唯一的区别是 foo8 的析构函数仅在我们到达 main() 的末尾时才被调用,而不是被在insert() 完成后立即调用。

    emplace()ing foo3 仅导致 1 次复制/移动构造函数调用(在 unordered_map 内部创建 foo10)并且仅调用 1 次 Foo 的析构函数。调用umap.emplace(foo3, d) 调用Foo 的非常量复制构造函数的原因如下:由于我们使用emplace(),编译器知道foo3(非常量Foo 对象)的意思成为某些Foo 构造函数的参数。在这种情况下,最合适的Foo 构造函数是非常量复制构造函数Foo(Foo&amp; f2)。这就是为什么umap.emplace(foo3, d) 调用复制构造函数而umap.emplace(11, d) 没有。

    对于foo11,我们直接将整数11 传递给emplace(11, d),以便unordered_mapemplace() 方法内执行时调用Foo(int) 构造函数。与 (2) 和 (3) 不同,我们甚至不需要一些预先存在的 foo 对象来执行此操作。重要的是,请注意只发生了 1 次对 Foo 构造函数的调用(它创建了 foo11)。

    然后我们直接将整数 12 传递给insert(12, d)。与emplace(11, d) 不同(召回导致仅对Foo 构造函数的一次调用),对insert(12, d) 的调用导致对Foo 的构造函数的两次调用(创建foo12foo13)。

结语:从这里去哪里?

一个。使用上面的源代码并学习在线找到的insert()(例如here)和emplace()(例如here)的文档。如果您使用的是 Eclipse 或 NetBeans 等 IDE,那么您可以轻松地让您的 IDE 告诉您正在调用 insert()emplace() 的哪个重载(在 eclipse 中,只需将鼠标光标稳定在函数调用上一秒)。这里还有一些代码可以尝试:

std::cout << "\numap.insert(" << Foo::foo_counter << ", d)\n";
umap.insert(Foo::foo_counter, d);
//but umap.emplace(Foo::foo_counter, d); results in a compile error!

std::cout << "\numap.insert(std::pair<const Foo, int>(" << Foo::foo_counter << ", d))\n";
umap.insert(std::pair<const Foo, int>(Foo::foo_counter, d));
//The above uses Foo(int) and then Foo(const Foo &), as expected. but the
// below call uses Foo(int) and the move constructor Foo(Foo&&). 
//Do you see why?
std::cout << "\numap.insert(std::pair<Foo, int>(" << Foo::foo_counter << ", d))\n";
umap.insert(std::pair<Foo, int>(Foo::foo_counter, d));
//Not only that, but even more interesting is how the call below uses all 
// three of Foo(int) and the Foo(Foo&&) move and Foo(const Foo &) copy 
// constructors, despite the below call's only difference from the call above 
// being the additional  .
std::cout << "\numap.insert(std::pair<Foo, int>(" << Foo::foo_counter << ", d))\n";
umap.insert(std::pair<Foo, int>(Foo::foo_counter, d));


//Pay close attention to the subtle difference in the effects of the next 
// two calls.
int cur_foo_counter = Foo::foo_counter;
std::cout << "\numap.insert(cur_foo_counter, d, cur_foo_counter+1, d) where " 
  << "cur_foo_counter = " << cur_foo_counter << "\n";
umap.insert(cur_foo_counter, d, cur_foo_counter+1, d);

std::cout << "\numap.insert(Foo::foo_counter, d, Foo::foo_counter+1, d) where "
  << "Foo::foo_counter = " << Foo::foo_counter << "\n";
umap.insert(Foo::foo_counter, d, Foo::foo_counter+1, d);


//umap.insert(std::initializer_list<std::pair<Foo, int>>(Foo::foo_counter, d));
//The call below works fine, but the commented out line above gives a 
// compiler error. It's instructive to find out why. The two calls
// differ by a "const".
std::cout << "\numap.insert(std::initializer_list<std::pair<const Foo, int>>(" << Foo::foo_counter << ", d))\n";
umap.insert(std::initializer_list<std::pair<const Foo, int>>(Foo::foo_counter, d));

您很快就会看到std::pair 构造函数的哪个重载(请参阅reference)最终被unordered_map 使用会对复制、移动、创建和/或复制对象的数量产生重要影响销毁以及这一切发生时。

b.看看当你使用其他容器类(例如std::setstd::unordered_multiset)而不是std::unordered_map 时会发生什么。

c。现在使用Goo 对象(只是Foo 的重命名副本)而不是int 作为unordered_map 中的范围类型(​​即使用unordered_map&lt;Foo, Goo&gt; 而不是unordered_map&lt;Foo, int&gt;),看看有多少和哪个Goo 构造函数被调用。 (剧透:有效果,但不是很戏剧化。)

【讨论】:

我认为值得一提的是,如果 Foo(int) 更改为 Foo(int, int) 之类的构造函数有多个参数,那么要实现类似于 umap.emplace(11, d) 的东西,我们可以使用 @ 987654460@ 和std::forward_as_tuple。所以声明将是umap.emplace(std::piecewise_construct, std::forward_as_tuple(11, 12), std::forward_as_tuple(d)); 【参考方案5】:

就功能或输出而言,它们都是相同的。

对于大内存,对象 emplace 是内存优化的,不使用复制构造函数

为了简单详细的解释 https://medium.com/@sandywits/all-about-emplace-in-c-71fd15e06e44

【讨论】:

Emplace 并非仅针对两个大内存进行内存优化,这就是我投反对票的原因。

以上是关于在 C++ 地图中插入 vs emplace vs operator[]的主要内容,如果未能解决你的问题,请参考以下文章

C++ emplace emplace_back是什么

C++ std::set emplace 返回值 first second

用vs2010,编译C++程序时,插入“__asm”代码显示3个错误,求高手解决!

Map vs Unordered_map——多线程

调试信息(断点等)存储在 VS2013、本机 C++ dll 项目中的位置在哪里?

将GMap封装为Activex供QT使用(工具:VS2017,QT5.12)