รหัสต่อไปนี้อาจช่วยให้คุณเข้าใจ "แนวคิดภาพรวม" ว่า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;
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));
std::cout << "\numap.insert(std::move(std::pair<Foo, int>(foo1, d)))\n";
umap.insert(std::move(std::pair<Foo, int>(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
's ก่อสร้างคัดลอก / ย้ายและ 2 โทรไปFoo
' s destructor (อย่างที่ผมอธิบาย):
ก. การแทรกfoo0
และfoo1
สร้างอ็อบเจ็กต์ชั่วคราว ( foo4
และfoo6
ตามลำดับ) ซึ่งตัวทำลายถูกเรียกใช้ทันทีหลังจากการแทรกเสร็จสิ้น นอกจากนี้Foo
s ภายในของ unordered_map (ซึ่งก็คือFoo
s 5 และ 7) ยังมีการเรียกตัวทำลายของมันเมื่อ unordered_map ถูกทำลาย
ข. ในการแทรกก่อนfoo2
อื่นเราได้สร้างวัตถุคู่ที่ไม่ใช่ชั่วคราว (เรียกว่าpair
) อย่างชัดเจนซึ่งเรียกว่าตัวFoo
สร้างการคัดลอกบนfoo2
(สร้างfoo8
เป็นสมาชิกภายในของpair
) จากนั้นเราinsert()
แก้ไขคู่นี้ซึ่งส่งผลให้unordered_map
เรียกตัวสร้างสำเนาอีกครั้ง (เปิดfoo8
) เพื่อสร้างสำเนาภายใน ( foo9
) ของตัวเอง เช่นเดียวกับfoo
s 0 และ 1 ผลลัพธ์ที่ได้คือตัวทำลายสองตัวที่เรียกการแทรกนี้โดยมีข้อแตกต่างเพียงอย่างเดียวfoo8
คือตัวทำลายของถูกเรียกก็ต่อเมื่อเราไปถึงจุดสิ้นสุดmain()
แทนที่จะถูกเรียกทันทีหลังจากinsert()
เสร็จ
การแทนที่foo3
ส่งผลให้มีการเรียกตัวสร้างสำเนา / ย้ายเพียง 1 รายการ (สร้างfoo10
ภายในในunordered_map
) และเพียง 1 การเรียกไปยังตัวFoo
ทำลายของ (ฉันจะกลับไปในภายหลัง)
สำหรับfoo11
เราส่งจำนวนเต็ม 11 โดยตรงไปemplace(11, d)
เพื่อที่unordered_map
จะเรียกตัวFoo(int)
สร้างในขณะที่การดำเนินการอยู่ในemplace()
เมธอด ซึ่งแตกต่างจากใน (2) และ (3) เราไม่จำเป็นต้องมีfoo
วัตถุก่อนออกเพื่อทำสิ่งนี้ด้วยซ้ำ ที่สำคัญสังเกตว่ามีการเรียกตัวFoo
สร้างเพียง 1 ครั้งเท่านั้น(ซึ่งสร้างขึ้นfoo11
)
จากนั้นเราจะผ่านจำนวนเต็ม 12 insert({12, d})
โดยตรงกับ ซึ่งแตกต่างจาก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()
และไม่มีการเรียกตัวสร้างการย้ายหรือคัดลอก)
หมายเหตุ: เหตุผลที่ " เกือบ " ใน " เกือบตลอดเวลา " ข้างต้นอธิบายไว้ใน I) ด้านล่าง
- ต่อ: สาเหตุที่การเรียก
umap.emplace(foo3, d)
ตัวFoo
สร้างการคัดลอกที่ไม่ใช่ const มีดังต่อไปนี้: เนื่องจากเราใช้emplace()
คอมไพเลอร์จึงรู้ว่าfoo3
( Foo
ออบเจ็กต์ที่ไม่ใช่ const ) หมายถึงอาร์กิวเมนต์สำหรับตัว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)
(ซึ่งเป็นโอเวอร์โหลด (2) ของinsert()
ในหน้า cppreference.com นี้) จะเทียบเท่ากับemplace(std::forward<P>(value))
เทียบเท่ากับ
II. จะไปที่ไหนจากที่นี่?
ก. ลองใช้ซอร์สโค้ดข้างต้นและเอกสารประกอบการศึกษาสำหรับinsert()
(เช่นที่นี่ ) และemplace()
(เช่นที่นี่ ) ที่พบได้ทั่วไป หากคุณใช้ IDE เช่น eclipse หรือ NetBeans คุณสามารถรับ IDE ของคุณเพื่อบอกคุณได้อย่างง่ายดายว่ามีการเรียกใช้งานเกินพิกัดinsert()
หรือemplace()
กำลังถูกเรียกใช้ (ใน eclipse เพียงแค่ให้เคอร์เซอร์ของเมาส์นิ่งเหนือการเรียกฟังก์ชันเป็นเวลาหนึ่งวินาที) นี่คือรหัสเพิ่มเติมที่จะลองใช้:
std::cout << "\numap.insert({{" << Foo::foo_counter << ", d}})\n";
umap.insert({{Foo::foo_counter, d}});
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}));
std::cout << "\numap.insert(std::pair<Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(std::pair<Foo, int>({Foo::foo_counter, d}));
std::cout << "\numap.insert({std::pair<Foo, int>({" << Foo::foo_counter << ", d})})\n";
umap.insert({std::pair<Foo, int>({Foo::foo_counter, d})});
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}});
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
สามารถมีผลสำคัญต่อจำนวนวัตถุที่ถูกคัดลอกย้ายสร้างและ / หรือทำลายรวมทั้งเมื่อสิ่งนี้เกิดขึ้นทั้งหมด
ข. ดูว่าจะเกิดอะไรขึ้นเมื่อคุณใช้คลาสคอนเทนเนอร์อื่น ๆ (เช่นstd::set
หรือstd::unordered_multiset
) แทนstd::unordered_map
แทน
ค. ตอนนี้ใช้Goo
วัตถุ (เพียงสำเนาที่เปลี่ยนชื่อFoo
) แทนint
เป็นประเภทช่วงในunordered_map
(เช่นใช้unordered_map<Foo, Goo>
แทนunordered_map<Foo, int>
) และดูจำนวนและตัวGoo
สร้างที่เรียกว่า (สปอยเลอร์: มีเอฟเฟกต์ แต่มันไม่ได้ดราม่ามาก)