以下代码可以帮助您理解insert() 与emplace() 的“大局观”。
代码摘要:Foo 类使用static int foo_counter 跟踪迄今为止已构造/移动的Foo 对象的总数。每个Foo 对象还将foo_counter 的值(在其创建时)存储在局部变量int val; 中,如果val 是8,那么Foo 对象将被称为“foo8”或“Foo 8”等。每次调用Foo 构造函数时,它都会输出有关对stdout 的调用的信息(例如,调用Foo(11) 将输出“Foo(int) with val: 11”)。 main() 中的代码打印到stdout 将执行的语句(例如umap.emplace(11, d))然后执行它。
#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;
int d; //Some int that will be umap's value. It is not important.
//Print the statement to be executed and then execute it.
std::cout << "\nFoo foo0, foo1, foo2, foo3;\n";
Foo foo0, foo1, foo2, foo3;
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 foo0, foo1, foo2, foo3;
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
这段代码及其输出显示了insert() 和emplace() 之间的主要“大图”区别是:
而使用insert() 几乎总是需要在main() 的范围内构造或预先存在一些Foo 对象(后跟复制或移动),如果使用@987654351 @ 然后对Foo 构造函数的任何调用完全在unordered_map 内部完成(即在emplace() 方法定义的范围内)。传递给emplace() 的键的参数直接转发到Foo 定义中的Foo 构造函数调用(可选的附加详细信息:此新构造的对象立即并入@987654358 之一@ 的成员变量,这样当执行离开 emplace() 时不会调用析构函数,也不会调用移动或复制构造函数。
注意:上述“几乎总是”中出现“几乎”的原因是因为insert()的一个重载实际上相当于emplace()。如in this cppreference.com page 所述,重载template<class P> std::pair<iterator, bool> insert(P&& value)(即此cppreference.com 页面上insert() 的重载(2))等效于emplace(std::forward<P>(value))。我不会再讨论这个特定的技术性问题。
我现在将详细介绍代码及其输出。
- 首先,请注意
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 对象想象为完全被动的。一旦您理解了这个“大局观”,重要的是要备份并了解unordered_map 对这个中介std::pair 对象的使用如何引入了微妙但重要的技术性。
-
insert()foo0、foo1 和 foo2 中的每一个都需要 2 次调用 Foo 的复制/移动构造函数之一和 2 次调用 Foo 的析构函数(就像我现在描述):
-
insert()ing 每个foo0 和foo1 创建了一个临时对象(分别为foo4 和foo6),然后在插入完成后立即调用其析构函数。此外,unordered_map 的内部Foos(foos 5 和 7)也调用了它们的析构函数,当 unordered_map 在执行到 main() 结束时被销毁。
- 为了
insert()foo2,我们首先显式创建了一个非临时pair对象(称为pair),它在foo2上调用Foo的复制构造函数(创建foo8作为内部pair 的成员)。然后我们insert()ed 这对,导致unordered_map 再次调用复制构造函数(在foo8 上)以创建自己的内部副本(foo9)。与 foos 0 和 1 一样,最终结果是对这个 insert()ion 的两次析构函数调用,唯一的区别是 foo8 的析构函数仅在我们到达 main() 的末尾时才被调用,而不是被在insert() 完成后立即调用。
-
emplace()ing foo3 仅导致 1 次复制/移动构造函数调用(在 unordered_map 内部创建 foo10)并且仅调用 1 次 Foo 的析构函数。调用umap.emplace(foo3, d) 调用Foo 的非常量复制构造函数的原因如下:由于我们使用emplace(),编译器知道foo3(非常量Foo 对象)的意思成为某些Foo 构造函数的参数。在这种情况下,最合适的Foo 构造函数是非常量复制构造函数Foo(Foo& f2)。这就是为什么umap.emplace(foo3, d) 调用复制构造函数而umap.emplace(11, d) 没有。
-
对于foo11,我们直接将整数11 传递给emplace(11, d),以便unordered_map 在emplace() 方法内执行时调用Foo(int) 构造函数。与 (2) 和 (3) 不同,我们甚至不需要一些预先存在的 foo 对象来执行此操作。重要的是,请注意只发生了 1 次对 Foo 构造函数的调用(它创建了 foo11)。
-
然后我们直接将整数 12 传递给insert({12, d})。与emplace(11, d) 不同(召回导致仅对Foo 构造函数的一次调用),对insert({12, d}) 的调用导致对Foo 的构造函数的两次调用(创建foo12 和foo13)。
结语:从这里去哪里?
一个。使用上面的源代码并学习在线找到的insert()(例如here)和emplace()(例如here)的文档。如果您使用的是 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 构造函数的哪个重载(请参阅reference)最终被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 构造函数被调用。 (剧透:有效果,但不是很戏剧化。)