Đoạn mã sau có thể giúp bạn hiểu "ý tưởng hình ảnh lớn" về sự insert()khác biệt của 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();
}
Đầu ra mà tôi nhận được là:
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
Thông báo rằng:
Một unordered_mapluôn luôn lưu trữ bên trong các Foođối tượng (và không, ví dụ, Foo *s) như các khóa, tất cả đều bị phá hủy khi unordered_mapbị phá hủy. Ở đây, các unordered_mapkhóa bên trong của foos là 13, 11, 5, 10, 7 và 9.
- Về mặt kỹ thuật, chúng tôi
unordered_mapthực sự lưu trữ std::pair<const Foo, int>các đối tượng, lần lượt lưu trữ các Foođối tượng. Nhưng để hiểu "ý tưởng hình ảnh lớn" về sự emplace()khác biệt như thế nào insert()(xem hộp được tô sáng bên dưới), bạn có thể tạm thời tưởng std::pairtượng đối tượng này là hoàn toàn thụ động. Khi bạn hiểu "ý tưởng hình ảnh lớn" này, điều quan trọng là phải sao lưu và hiểu cách sử dụng std::pairđối tượng trung gian này bằng cách unordered_mapgiới thiệu các kỹ thuật tinh tế, nhưng quan trọng.
Chèn mỗi foo0, foo1và foo2yêu cầu 2 cuộc gọi đến một trong những Foo's constructor sao chép / di chuyển và 2 cuộc gọi đến Foo' s destructor (như bây giờ tôi mô tả):
a. Chèn từng foo0và foo1tạo một đối tượng tạm thời ( foo4và foo6, tương ứng) có hàm hủy của nó ngay lập tức được gọi sau khi chèn xong. Ngoài ra, các nội bộ của unordered_map Foo(là Foos 5 và 7) cũng có các hàm hủy của chúng được gọi khi unordered_map bị hủy.
b. Để chèn foo2, thay vào đó, trước tiên chúng tôi rõ ràng đã tạo một đối tượng cặp không tạm thời (được gọi pair), được gọi Foolà hàm tạo sao chép trên foo2(tạo foo8như một thành viên nội bộ của pair). Sau đó, chúng tôi đã chỉnh sửa insert()cặp này, dẫn đến việc unordered_mapgọi lại hàm tạo sao chép (bật foo8) để tạo bản sao nội bộ của chính nó ( foo9). Như với foos 0 và 1, kết quả cuối cùng là hai lệnh gọi hàm hủy đối với phần chèn này với sự khác biệt duy nhất foo8là hàm hủy đó chỉ được gọi khi chúng ta kết thúc main()thay vì được gọi ngay sau khi insert()kết thúc.
Việc đặt foo3kết quả chỉ dẫn đến 1 cuộc gọi sao chép / di chuyển của nhà xây dựng (tạo foo10nội bộ trong unordered_map) và chỉ có 1 cuộc gọi đến hàm Foohủy của. (Tôi sẽ quay lại vấn đề này sau).
Đối với foo11, chúng tôi đã trực tiếp chuyển số nguyên 11 đến emplace(11, d)mức unordered_mapsẽ gọi hàm Foo(int)tạo trong khi thực thi nằm trong emplace()phương thức của nó . Không giống như trong (2) và (3), chúng tôi thậm chí không cần một số foođối tượng thoát trước để làm điều này. Điều quan trọng, lưu ý rằng chỉ có 1 cuộc gọi đến một nhà Fooxây dựng xảy ra (mà đã tạo foo11).
Sau đó chúng tôi trực tiếp chuyển số nguyên 12 đến insert({12, d}). Không giống như emplace(11, d)(việc gọi lại chỉ dẫn đến 1 cuộc gọi đến hàm Footạo), cuộc gọi này insert({12, d})dẫn đến hai cuộc gọi đến hàm Footạo (tạo foo12và foo13).
Điều này cho thấy sự khác biệt "bức tranh lớn" chính giữa insert()và emplace()là:
Trong khi việc sử dụng insert() hầu như luôn đòi hỏi việc xây dựng hoặc tồn tại một số Foođối tượng trong main()phạm vi (theo sau là bản sao hoặc di chuyển), nếu sử dụng emplace()thì bất kỳ lệnh gọi nào đến hàm Footạo đều được thực hiện hoàn toàn trong unordered_map(nghĩa là trong phạm vi emplace()định nghĩa của phương thức). (Các) đối số cho khóa mà bạn chuyển đến emplace()được chuyển tiếp trực tiếp đến Foolệnh gọi của hàm tạo trong unordered_map::emplace()định nghĩa của nó (chi tiết bổ sung tùy chọn: trong đó đối tượng mới được xây dựng này được kết hợp ngay lập tức vào một trong unordered_mapcác biến thành viên để không có hàm hủy nào được gọi khi lá thực thi emplace()và không có nhà xây dựng di chuyển hoặc sao chép được gọi).
Lưu ý: Lý do " gần như " trong " hầu như luôn luôn " ở trên được giải thích trong I) bên dưới.
- tiếp tục: Lý do tại sao gọi điện thoại
umap.emplace(foo3, d)gọi Foo's constructor sao chép không const như sau: Kể từ khi chúng tôi đang sử dụng emplace(), trình biên dịch biết rằng foo3(một tổ chức phi const Fooobject) được hiểu là một cuộc tranh cãi với một số Foonhà xây dựng. Trong trường hợp này, hàm tạo phù hợp nhất Foolà hàm tạo sao chép không phải là const Foo(Foo& f2). Đây là lý do tại sao umap.emplace(foo3, d)được gọi là một nhà xây dựng sao chép trong khi umap.emplace(11, d)không.
Phần kết:
I. Lưu ý rằng một quá tải insert()thực sự tương đương với emplace() . Như được mô tả trong trang cppreference.com này , tình trạng quá tải template<class P> std::pair<iterator, bool> insert(P&& value)(quá tải (2) của insert()trang cppreference.com này) tương đương với emplace(std::forward<P>(value)).
II. Đi đâu từ đây?
a. Chơi xung quanh với mã nguồn ở trên và tài liệu nghiên cứu cho insert()(ví dụ ở đây ) và emplace()(ví dụ ở đây ) được tìm thấy trực tuyến. Nếu bạn đang sử dụng một IDE như nhật thực hoặc NetBeans thì bạn có thể dễ dàng yêu cầu IDE của bạn cho bạn biết tình trạng quá tải insert()hoặc emplace()đang được gọi (trong nhật thực, chỉ cần giữ con trỏ chuột ổn định trong lệnh gọi trong giây). Dưới đây là một số mã để thử:
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}}));
Bạn sẽ sớm thấy rằng quá tải của hàm std::pairtạo (xem tài liệu tham khảo ) cuối cùng được sử dụng unordered_mapcó thể có ảnh hưởng quan trọng đến số lượng đối tượng được sao chép, di chuyển, tạo và / hoặc phá hủy cũng như khi tất cả điều này xảy ra.
b. Xem điều gì xảy ra khi bạn sử dụng một số lớp container khác (ví dụ std::sethoặc std::unordered_multiset) thay vì std::unordered_map.
c. Bây giờ sử dụng một Goođối tượng (chỉ là một bản sao được đổi tên của Foo) thay vì một intloại phạm vi trong một unordered_map(nghĩa là sử dụng unordered_map<Foo, Goo>thay vì unordered_map<Foo, int>) và xem có bao nhiêu và hàm Gootạo nào được gọi. (Spoiler: có một hiệu ứng nhưng nó không ấn tượng lắm.)