在c ++ map中插入vs emplace vs operator
我第一次使用地图,我意识到有很多方法来插入一个元素。 您可以使用emplace()
, operator[]
或insert()
,以及使用value_type
或make_pair
等变体。 虽然关于所有这些问题以及关于特殊情况的问题都有很多信息,但是我仍然不能理解这个大局。 所以,我的两个问题是:
-
他们每个人的优点是什么?
-
有没有必要为标准添加emplace? 没有它之前有什么是不可能的吗?
在地图的特定情况下,旧的选项只有两个: operator[]
和insert
( insert
不同风味)。 所以我会开始解释这些。
operator[]
是查找或添加运算符。 它会尝试在地图中查找具有给定键的元素,如果存在,则会返回对存储值的引用。 如果没有,它会创build一个新的元素插入到位,默认初始化并返回一个引用。
insert
函数(在单个元素的flavor中)需要一个value_type
( std::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
,而是将t
和u
对象的引用传递给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
请注意:
-
一个
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
对象引入微妙而重要的技术。
- 所以在技术上,我们的
-
插入
foo0
,foo1
和foo2
每一个都需要2个对Foo
的复制/移动构造函数的调用和2个对Foo
的析构函数的调用(如我现在描述的):一个。 插入
foo0
和foo1
每一个foo0
创build一个临时对象(分别为foo4
和foo6
),然后在插入完成后立即调用析构函数。 另外,当unordered_map被销毁时,unordered_map的内部Foo
(foos 5和7)也有它们的析构函数。湾 为了插入
foo2
,我们首先创build了一个名为Foo
的拷贝构造函数的非临时对(foo8
),然后插入它,导致unordered_map再次调用拷贝构造函数来创build一个内部拷贝(foo9
)。 与foos 0和1一样,最终的结果是两个析构函数调用这个插入,唯一的区别是只有当我们到达main()
的末尾时才调用foo8
的析构函数。 -
放置
foo3
导致只有1个复制/移动构造函数调用(内部创buildfoo10
),只有1个调用Foo
的析构函数。 (我稍后会回到这个)。 -
对于
foo11
,我们直接传递了整数11,所以unordered_map
会调用Foo(int)
构造函数。 与(3)不同,我们甚至不需要一些事先存在的foo对象来做到这一点。
这显示了insert()
和emplace()
之间的主要“大图”区别是:
而使用
insert()
几乎总是需要在main()
的作用域(后跟一个拷贝或移动)中构造或存在某个Foo
对象,如果使用emplace()
那么任何对构造函数的调用完全是在unordered_map
(即在emplace()
方法定义的范围内)。 您传递给emplace()
的键的参数直接转发到unordered_map
的Foo
构造函数调用(可选的更多细节:这个新构造的对象立即被合并到unordered_map
的成员variables之一中,从而没有析构函数在执行离开emplace()
时被调用,并且不调用移动或复制构造函数)。
注: 几乎总是在下面的I)中解释的原因。
- 继续:调用
umap.emplace(foo3, d)
调用Foo
的非const拷贝构造函数的原因如下:由于我们使用emplace()
,编译器知道foo3
(非constFoo
对象)是意图成为一些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::set
或std::unordered_multiset
)而不是std::unordered_map
。
C。 现在使用Goo
对象(只是Foo
的重命名副本)而不是int
作为unordered_map
的范围types(即,使用unordered_map<Foo, Goo>
而不是unordered_map<Foo, int>
),并查看有多less个Goo
构造函数叫做。 (剧透:有效果,但不是很戏剧性。)