在c ++ map中插入vs emplace vs operator

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

  1. 他们每个人的优点是什么?

  2. 有没有必要为标准添加emplace? 没有它之前有什么是不可能的吗?

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

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

insert函数(在单个元素的flavor中)需要一个value_typestd::pair<const Key,Value> ),它使用键( first成员)并尝试插入它。 因为std::map不允许重复,如果有一个现有的元素它不会插入任何东西。

两者之间的第一个区别是operator[]需要能够构造默认的初始化 ,因此对于不能被默认初始化的值types是不可用的。 两者之间的第二个区别是当已经有一个给定键的元素时会发生什么。 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一个对象,可以用不同的方式创build。 你可以用适当的types直接构造它,或者传递任何可以构造value_type对象,这就是std::make_pair进入的地方,因为它允许简单地创buildstd::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]实际上是相同的。 在这两种情况下,代码都会创build一个相同types的临时对象( std::pair<const K,V> ),并将其传递给insert函数。 insert函数将在二叉search树中创build适当的节点,然后将value_type部分从参数复制到节点。 使用value_type的好处在于, value_type总是匹配 value_type ,所以不能输错std::pair参数的types!

区别在于[3]。 函数std::make_pair是一个将创build一个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<K,V> (注意缺less的const )。 该签名要求value_type与从std::make_pair的调用返回的值相近但不相同。 因为它足够接近它将创build一个正确的types的临时和复制初始化它。 这又会被复制到节点,总共创build两个副本。

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

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

但是,这仍然是错误的,就像在[1]中明确键入types一样。

到目前为止,我们有不同的方法来调用insert ,它需要在外部创buildvalue_type ,并将该对象的副本放入容器中。 或者,如果types是默认的可构造的可赋值的 (有意仅聚焦于m[k]=v ),则可以使用operator[] ,并且需要默认初始化一个对象并将该值复制到该对象中。

在C ++ 11中,通过可变参数模板和完美的转发,通过置换 (在原地创build)的方式将元素添加到容器中有一种新的方法。 在不同的容器中的emplace函数基本上是一样的:函数取得的参数将被转发到存储在容器中的对象的构造函数,而不是从其中复制到容器中的

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

在[5]中,不会创buildstd::pair<const K, V>并将其传递给emplace ,而是将tu对象的引用传递给emplace ,并将它们转发给数据中的value_type子对象的构造函数结构体。 在这种情况下, std::pair<const K,V>完全没有副本,这是在C ++ 03选项上的优势。 在insert的情况下,它不会覆盖地图中的值。


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

Emplace:利用右值引用来使用已经创build的实际对象。 这意味着没有复制或移动构造函数被调用,对于大对象很有用! O(log(N))时间。

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

Operator []:检查对象是否存在,如果存在,则修改对该对象的引用,否则使用提供的键和值对两个对象调用make_pair,然后执行与insert函数相同的工作。 这是O(log(N))时间。

make_pair:只不过是一对。

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

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

这里有一个例子来演示:

 #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 } 

这是一个非常具体的细节,但是当你处理用户定义的转换链时,值得记住这一点。

下面的代码可以帮助你理解insert()emplace()不同之处:

 #include <iostream> #include <unordered_map> #include <utility> struct Foo { static int foo_counter; int val; 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; Foo foo0, foo1, foo2, foo3; int d; std::cout << "\numap.insert(std::pair<Foo, int>(foo0, d))\n"; umap.insert(std::pair<Foo, int>(foo0, d)); //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))); //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() 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 

请注意:

  1. 一个unordered_map总是在内部存储Foo对象(而不是Foo * s)作为关键字,当unordered_map被销毁时这些对象全部被销毁。 这里, unordered_map的内部键是foos 13,11,5,10,7和9。

    • 所以在技术上,我们的unordered_map实际上存储了std::pair<const Foo, int>对象,它们依次存储Foo对象。 但是为了理解emplace()insert() (见下面突出显示的方框emplace()不同之处,可以暂时想象这个std::pair对象是完全被动的。 一旦你理解了这个“大局观念”,重要的是要备份和理解unordered_map如何使用这个中间std::pair对象引入微妙而重要的技术。
  2. 插入foo0foo1foo2每一个都需要2个对Foo的复制/移动构造函数的调用和2个对Foo的析构函数的调用(如我现在描述的):

    一个。 插入foo0foo1每一个foo0创build一个临时对象(分别为foo4foo6 ),然后在插入完成后立即调用析构函数。 另外,当unordered_map被销毁时,unordered_map的内部Foo (foos 5和7)也有它们的析构函数。

    湾 为了插入foo2 ,我们首先创build了一个名为Foo的拷贝构造函数的非临时对( foo8 ),然后插入它,导致unordered_map再次调用拷贝构造函数来创build一个内部拷贝( foo9 )。 与foos 0和1一样,最终的结果是两个析构函数调用这个插入,唯一的区别是只有当我们到达main()的末尾时才调用foo8的析构函数。

  3. 放置foo3导致只有1个复制/移动构造函数调用(内部创buildfoo10 ),只有1个调用Foo的析构函数。 (我稍后会回到这个)。

  4. 对于foo11 ,我们直接传递了整数11,所以unordered_map会调用Foo(int)构造函数。 与(3)不同,我们甚至不需要一些事先存在的foo对象来做到这一点。

这显示了insert()emplace()之间的主要“大图”区别是:

而使用insert() 几乎总是需要在main()的作用域(后跟一个拷贝或移动)中构造或存在某个Foo对象,如果使用emplace()那么任何对构造函数的调用完全是在unordered_map (即在emplace()方法定义的范围内)。 您传递给emplace()的键的参数直接转发到unordered_mapFoo构造函数调用(可选的更多细节:这个新构造的对象立即被合并到unordered_map的成员variables之一中,从而没有析构函数在执行离开emplace()时被调用,并且不调用移动或复制构造函数)。

注: 几乎总是在下面的I)中解释的原因。

  1. 继续:调用umap.emplace(foo3, d)调用Foo的非const拷贝构造函数的原因如下:由于我们使用emplace() ,编译器知道foo3 (非const Foo对象)是意图成为一些Foo构造函数的参数。 在这种情况下,最合适的Foo构造函数是非const复制构造Foo(Foo& f2) 。 这就是为什么umap.emplace(foo3, d)调用复制构造函数,而umap.emplace(11, d)没有。

结语:

I.注意insert()一个重载实际上等同于 emplace() 。 如此cppreference.com页面中所述,重载template<class P> std::pair<iterator, bool> insert(P&& value) (这是在此cppreference.com页面上insert()重载(2))是等价的emplace(std::forward<P>(value))

II。 然后去哪儿?

一个。 玩上面的源代码和学习文档insert() (例如这里 )和emplace() (例如这里 ),这是在网上find的。 如果您使用的是IDE(例如eclipse或NetBeans),那么您可以轻松地让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 to 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 cals // 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构造函数(请参见参考资料 )的哪个重载最终被unordered_map使用,可以对复制,移动,创build和/或销毁多less个对象以及何时发生这种情况产生重要影响。

湾 看看当你使用一些其他的容器类(例如std::setstd::unordered_multiset )而不是std::unordered_map

C。 现在使用Goo对象(只是Foo的重命名副本)而不是int作为unordered_map的范围types(​​即,使用unordered_map<Foo, Goo>而不是unordered_map<Foo, int> ),并查看有多less个Goo构造函数叫做。 (剧透:有效果,但不是很戏剧性。)