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

时间:2013-06-18 14:52:30

标签: c++ dictionary insert operators emplace

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

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

  2. 是否需要在标准中添加安全性?没有它之前有什么不可能的吗?

5 个答案:

答案 0 :(得分:170)

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

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

insert函数(在单个元素的味道中)采用value_typestd::pair<const Key,Value>),它使用键(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<const K,V>)并将其传递给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<K,V>(请注意缺少的const })。签名需要value_type close ,但与调用std::make_pair的返回值不同。因为它足够接近它会创建一个正确类型的临时类并复制初始化它。然后将其复制到节点,共创建两个副本。

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

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

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

到目前为止,我们有不同的方式调用insert,需要在外部创建value_type,并将该对象的副本放入容器中。或者,如果类型默认可构造可分配(故意仅关注operator[]),则可以使用m[k]=v,并且它需要默认初始化一个对象和该对象的值的 copy

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

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

在[5]中,std::pair<const K, V>未创建并传递给emplace,而是将tu对象的引用传递给emplace将它们转发到数据结构中value_type子对象的构造函数。在这种情况下,完成std::pair<const K,V> no 副本,这是emplace优于C ++ 03替代方案的优势。与insert的情况一样,它不会覆盖地图中的值。


我没有想过的一个有趣的问题是如何为地图实际实现emplace,这在一般情况下不是一个简单的问题。

答案 1 :(得分:11)

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

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

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

make_pair:只需成对一对。

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

答案 2 :(得分:10)

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

以下是一个示例:

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

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

答案 3 :(得分:9)

以下代码可以帮助您了解&#34;大图片的想法&#34; insert()emplace()的区别:

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

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;  
    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的内部密钥分别为13,11,5,10,7和9。

    • 从技术上讲,我们的unordered_map实际上存储了std::pair<const Foo, int>个对象,而这些对象又存储了Foo个对象。但是要理解&#34;大图片的想法&#34; emplace()insert()的区别(见下面突出显示的方框),暂时将此std::pair对象想象为完全被动,这是可以的。一旦你理解了这个&#34;全局的想法,&#34;然后重要的是备份并理解std::pairunordered_map对象的使用如何引入微妙但重要的技术性。
  2. foo0foo1foo2中的每一个插入Foo的一个{/ 1}}复制/移动构造函数和2个调用中需要2个调用到Foo的析构函数(我现在描述):

    一个。插入foo0foo1中的每一个分别创建了一个临时对象(foo4foo6),然后在插入完成后立即调用析构函数。另外,当unordered_map被破坏时,unordered_map的内部Foo(第5和第7)也会调用它们的析构函数。

    湾要插入foo2,我们首先显式创建一个非临时对对象(称为pair),在Foo上调用foo2的复制构造函数(创建foo8作为pair的内部成员。然后我们insert()编辑了这一对,这导致unordered_map再次调用复制构造函数(在foo8上)以创建自己的内部副本(foo9)。与foo的0和1一样,最终结果是对此插入的两个析构函数调用,唯一的区别是foo8的析构函数仅在我们到达{{{{}时才被调用1}}而不是在main()完成后立即调用。

  3. 安排insert()仅导致1次复制/移动构造函数调用(在foo3内部创建foo10),只有1次调用unordered_map&#39 ;析构函数。 (我稍后会再回过头来看看。)

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

  5. 然后我们直接将整数12传递给Foo。与insert({12, d})不同,此emplace(11, d)调用导致两次调用Foo的构造函数。

  6. 这显示了主要的&#34;大图片&#34; insert({12, d})insert()之间的差异是:

      

    使用emplace() 几乎总是需要在insert()的范围内构建或存在一些Foo对象(后跟副本或移动),如果使用main()那么对emplace()构造函数的任何调用都是在Foo内完全内部完成的(即在unordered_map方法定义的范围内)。您传递给emplace()的密钥的参数将直接转发到emplace()内的Foo构造函数调用(可选的其他详细信息:此新构建的对象立即合并到一个unordered_map的成员变量,以便在执行离开unordered_map时不会调用析构函数,并且不会调用移动或复制构造函数。)

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

    1. 续:调用emplace()调用umap.emplace(foo3, d)的非const复制构造函数的原因如下:由于我们正在使用Foo,编译器知道emplace()(非const foo3对象)意味着某些Foo构造函数的参数。在这种情况下,最合适的Foo构造函数是非const复制构造Foo。这就是为Foo(Foo& f2)调用复制构造函数而umap.emplace(foo3, d)没有调用的原因。
    2. 结语:

      予。请注意,umap.emplace(11, d)的一次重载实际上等同于 insert()。如上所述in this cppreference.com page,重载emplace()(此cppreference.com页面上template<class P> std::pair<iterator, bool> insert(P&& value)的重载(2))相当于insert()

      II。然后去哪儿?

      一个。使用上面的emplace(std::forward<P>(value))(例如here}和insert()(例如here)在线找到的源代码和研究文档。如果您正在使用诸如eclipse或NetBeans之类的IDE,那么您可以轻松地让IDE告诉您正在调用emplace()insert()的哪个重载(在eclipse中,只需保留鼠标&# 39; s光标稳定在函数调用上一秒钟)。这里还有一些代码可以试用:

      emplace()

      您很快就会发现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}})); 构造函数的哪个重载(请参阅reference)最终被std::pair使用,这会对复制的对象数量产生重要影响,移动,创建和/或销毁,以及何时发生。

      湾查看使用其他容器类(例如unordered_mapstd::set)而不是std::unordered_multiset时会发生什么。

      ℃。现在使用std::unordered_map对象(只是重命名的Goo副本)而不是Foo作为int中的范围类型(​​即使用unordered_map代替unordered_map<Foo, Goo>)并查看调用了多少unordered_map<Foo, int>Goo构造函数。 (剧透:有效果,但不是很戏剧性。)

答案 4 :(得分:0)

在功能或输出方面,它们都是相同的。

对于两个大内存,对象位置都是内存优化的,不使用副本构造器

简单的详细说明 https://medium.com/@sandywits/all-about-emplace-in-c-71fd15e06e44