Đ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_map
luô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_map
bị phá hủy. Ở đây, các unordered_map
khó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_map
thự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::pair
tượ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_map
giới thiệu các kỹ thuật tinh tế, nhưng quan trọng.
Chèn mỗi foo0
, foo1
và foo2
yê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 foo0
và foo1
tạo một đối tượng tạm thời ( foo4
và 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à Foo
s 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 Foo
là hàm tạo sao chép trên foo2
(tạo foo8
như 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_map
gọ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 foo
s 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 foo8
là 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 foo3
kế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 foo10
nội bộ trong unordered_map
) và chỉ có 1 cuộc gọi đến hàm Foo
hủ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_map
sẽ 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à Foo
xâ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 Foo
tạo), cuộc gọi này insert({12, d})
dẫn đến hai cuộc gọi đến hàm Foo
tạo (tạo foo12
và 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 Foo
tạ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 Foo
lệ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_map
cá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 Foo
object) được hiểu là một cuộc tranh cãi với một số Foo
nhà xây dựng. Trong trường hợp này, hàm tạo phù hợp nhất Foo
là 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::pair
tạo (xem tài liệu tham khảo ) cuối cùng được sử dụng unordered_map
có 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::set
hoặ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 int
loạ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 Goo
tạo nào được gọi. (Spoiler: có một hiệu ứng nhưng nó không ấn tượng lắm.)