在C ++ Map中插入vs Emplace vs operator []


188

我第一次使用地图,我意识到有很多插入元素的方法。您可以使用emplace()operator[]或者insert(),再加上喜欢使用的变体value_typemake_pair。尽管关于它们的信息很多,也有关于特定案例的问题,但我仍然无法理解大局。因此,我的两个问题是:

  1. 他们每个人比其他人有什么优势?

  2. 是否有需要将Emplace添加到标准中?没有它,有什么是不可能的吗?


1
位置语义允许显式转换和直接初始化。
Kerrek SB 2013年

3
现在operator[]基于try_emplace。也许值得一提insert_or_assign
FrankHB

@FrankHB如果您(或其他人)添加了最新答案,我可以更改接受的答案。
德国Capuano

Answers:


226

在地图的特定情况下,旧选项只有两个:operator[]insert(的口味不同insert)。因此,我将开始解释这些。

operator[]是一个发现-或-加运算符。它将尝试在地图内查找具有给定键的元素,如果存在,它将返回对存储值的引用。如果没有,它将创建一个默认插入的新元素,并返回对其的引用。

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将返回a std::pair<K,V>(请注意const)。签名要求value_type接近,但不一样的从调用返回的值std::make_pair。因为它足够接近,所以它将创建正确类型的临时文件并对其进行复制初始化。依次将其复制到节点,总共创建两个副本。

可以通过提供模板参数来解决此问题:

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

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

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

在C ++ 11中,具有可变参数的模板和完美的转发功能,这是一种通过嵌入(在本地创建)将元素添加到容器中的新方法。emplace不同容器中的函数基本上具有相同的作用:与其获取从其复制到容器中的,该函数采用将被转发到存储在容器中的对象的构造函数的参数。

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

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


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


5
答案中暗示了这一点,但是map [] = val将覆盖先前的值(如果存在)。
dk123

在我看来,一个更有趣的问题是它没有什么作用。因为保存了对副本,所以很好,因为没有对副本意味着没有mapped_type非副本。我们想要的是将对的构造mapped_type放置在对中,并将对的构造放置在地图中。因此,缺少该std::pair::emplace功能及其转发支持map::emplace。在当前形式下,您仍然必须将构造的appedd_type赋予对构造函数,该构造函数将对其进行一次复制。它比两倍好,但仍然没有好处。
v.oddou

实际上,我修改了该注释,在C ++ 11中,有一个模板对构造函数,其作用与在1个参数构造的情况下完全相同。以及一些奇怪的分段构造(如他们所说的),使用元组来转发参数,因此我们似乎仍然可以完美地转发它。
v.oddou 2014年

看起来在unordered_map和map中存在insert的性能错误:链接
Deqing

1
可能是好的,对信息进行更新这个insert_or_assigntry_emplace(无论是从C ++ 17),这有助于填补一些空白的功能从现有的方法。
ShadowRanger

14

Emplace:利用右值引用来使用已经创建的实际对象。这意味着不会调用任何复制或移动构造函数,这对大型对象来说非常有用!O(log(N))时间。

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

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

make_pair:只做一对。

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


4
无需将Emplace添加到标准中。” 这显然是错误的。emplace()只是插入无法复制或移动的元素的唯一方法。(&是的,也许,如果存在这种情况,最有效地插入其复制和移动构造函数比构造要花费很多的代码),似乎您也弄错了主意:这与“ [利用]右值引用无关” 使用您已经创建的实际对象 ”;没有对象尚未创建,与你转发map的参数需要创建它里面本身。您不做对象。
underscore_d

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
}

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


想象一下,foo在其ctor中需要两个整数,而不是一个。您可以使用此通话吗? v.emplace(v.end(), 10, 10); ...或者您现在需要使用: v.emplace(v.end(), foo(10, 10) ); 吗?
Kaitain 2015年

我现在没有访问编译器的权限,但是我假设这意味着这两个版本都可以使用。您看到的几乎所有示例emplace都使用带有单个参数的类。IMO如果在示例中使用多个参数,则实际上将使emplace可变参数语法的性质更加清楚。
Kaitain 2015年

9

以下代码可以帮助您理解与以下方面有何insert()不同的“大概念” emplace()

#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;  
    Foo foo0, foo1, foo2, foo3;
    int d;

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

    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() 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()(请参见下面的突出显示的方框),可以暂时将这个std::pair对象想象成完全是被动的。一旦理解了这个“大概念”,就必须备份并了解如何std::pair通过unordered_map引入微妙但重要的技术来使用此中介对象,这一点很重要。
  2. 将每个的foo0foo1以及foo2需要2调用之一Foo的复制/移动的构造和图2点的调用Foo的析构函数(如我现在描述):

    一个。插入foo0foo1创建一个临时对象(分别为foo4foo6),然后在完成插入后立即调用其析构函数。此外,在销毁unordered_map时,unordered_map的内部Foos(分别为Foo5和7)也调用了它们的析构函数。

    b。要插入foo2,我们首先明确创建了一个非临时对对象(称为pair),该对象称为Foo的复制构造函数on foo2foo8作为的内部成员创建pair)。然后insert(),我们编辑了这对,导致unordered_map再次(在上foo8)调用复制构造函数以创建其自己的内部副本(foo9)。与foos 0和1一样,最终结果是对该插入操作进行了两次析构函数调用,唯一的不同是,foo8只有在我们到达的末尾才调用的析构函数,main()而不是在insert()完成后立即调用。

  3. 嵌入foo3仅导致1个copy / move构造函数调用(在中foo10内部创建unordered_map),而Foo对的析构函数只有1个调用。(我稍后会再讲)。

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

  5. 然后,我们直接将整数12传递给insert({12, d})。与with不同emplace(11, d)(该调用仅导致对Foo构造函数的1次调用),而对的调用insert({12, d})导致对Foo的构造函数两次调用(创建foo12foo13)。

这表明insert()和之间的主要“全局”区别emplace()是:

使用insert() 几乎总是需要在范围内构造或存在某个Foo对象main()(随后是复制或移动),如果使用,emplace()则对Foo构造函数的任何调用都完全在内部unordered_map(即emplace()方法定义范围内)进行。传递给您的键的参数emplace()会直接转发到的定义中的Foo构造函数调用unordered_map::emplace()(可选的其他详细信息:其中,此新构造的对象会立即合并到unordered_map的成员变量中,因此在调用时不会调用析构函数执行离开emplace(),不会调用move或copy构造函数)。

注意:上面“ 几乎总是 ”中“ 几乎 ” 的原因在下面的I)中进行了说明。

  1. 续:之所以调用umap.emplace(foo3, d)被称为Foo的非常量副本构造函数的原因如下:由于我们使用emplace(),所以编译器知道foo3(一个非常量Foo对象)是某些Foo构造函数的参数。在这种情况下,最合适的Foo构造函数是non-const copy构造函数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页上的重载(2)insert())等效于emplace(std::forward<P>(value))

二。然后去哪儿?

一个。试一下上面的源代码,并研究insert()(例如此处)和emplace()(例如此处)在线提供的文档。如果您使用的是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构造函数(请参阅参考资料)最终被哪个重载使用,unordered_map可以对复制,移动,创建和/或销毁多少对象以及何时发生所有对象产生重要影响。

b。看看使用其他容器类(例如std::setstd::unordered_multiset)代替时会发生什么std::unordered_map

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


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.