以下代码可以帮助您理解与以下方面有何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
注意:
一个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
引入微妙但重要的技术来使用此中介对象,这一点很重要。
将每个的foo0
,foo1
以及foo2
需要2调用之一Foo
的复制/移动的构造和图2点的调用Foo
的析构函数(如我现在描述):
一个。插入foo0
并foo1
创建一个临时对象(分别为foo4
和foo6
),然后在完成插入后立即调用其析构函数。此外,在销毁unordered_map时,unordered_map的内部Foo
s(分别为Foo
5和7)也调用了它们的析构函数。
b。要插入foo2
,我们首先明确创建了一个非临时对对象(称为pair
),该对象称为Foo
的复制构造函数on foo2
(foo8
作为的内部成员创建pair
)。然后insert()
,我们编辑了这对,导致unordered_map
再次(在上foo8
)调用复制构造函数以创建其自己的内部副本(foo9
)。与foo
s 0和1一样,最终结果是对该插入操作进行了两次析构函数调用,唯一的不同是,foo8
只有在我们到达的末尾才调用的析构函数,main()
而不是在insert()
完成后立即调用。
嵌入foo3
仅导致1个copy / move构造函数调用(在中foo10
内部创建unordered_map
),而Foo
对的析构函数只有1个调用。(我稍后会再讲)。
对于foo11
,我们直接将整数11传递给,emplace(11, d)
以便在执行处于其方法内时unordered_map
调用Foo(int)
构造函数emplace()
。与(2)和(3)不同,我们甚至不需要一些预先存在的foo
对象来执行此操作。重要的是,请注意,仅对Foo
构造函数进行了1次调用(创建了foo11
)。
然后,我们直接将整数12传递给insert({12, d})
。与with不同emplace(11, d)
(该调用仅导致对Foo
构造函数的1次调用),而对的调用insert({12, d})
导致对Foo
的构造函数两次调用(创建foo12
和foo13
)。
这表明insert()
和之间的主要“全局”区别emplace()
是:
使用insert()
几乎总是需要在范围内构造或存在某个Foo
对象main()
(随后是复制或移动),如果使用,emplace()
则对Foo
构造函数的任何调用都完全在内部unordered_map
(即emplace()
方法定义范围内)进行。传递给您的键的参数emplace()
会直接转发到的定义中的Foo
构造函数调用unordered_map::emplace()
(可选的其他详细信息:其中,此新构造的对象会立即合并到unordered_map
的成员变量中,因此在调用时不会调用析构函数执行离开emplace()
,不会调用move或copy构造函数)。
注意:上面“ 几乎总是 ”中“ 几乎 ” 的原因在下面的I)中进行了说明。
- 续:之所以调用
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::set
或std::unordered_multiset
)代替时会发生什么std::unordered_map
。
C。现在,使用Goo
对象(只是的重命名副本Foo
),而不是int
作为对象中的范围类型unordered_map
(即,使用unordered_map<Foo, Goo>
代替unordered_map<Foo, int>
),并查看Goo
调用了多少个构造函数。(剧透:有效果,但效果不是很大。)