แทรก vs emplace เทียบกับตัวดำเนินการ [] ในแผนที่ c ++


207

ฉันใช้แผนที่เป็นครั้งแรกและฉันตระหนักว่ามีหลายวิธีในการแทรกองค์ประกอบ คุณสามารถใช้emplace(), operator[]หรือinsert()บวกสายพันธุ์เช่นการใช้หรือvalue_type make_pairแม้ว่าจะมีข้อมูลมากมายเกี่ยวกับพวกเขาทั้งหมดและคำถามเกี่ยวกับบางกรณี แต่ฉันก็ยังไม่เข้าใจภาพรวม ดังนั้นสองคำถามของฉันคือ:

  1. อะไรคือข้อได้เปรียบของแต่ละคนที่มีมากกว่าคนอื่น ๆ ?

  2. จำเป็นต้องเพิ่ม emplace ลงในมาตรฐานหรือไม่? มีอะไรที่เป็นไปไม่ได้มาก่อนโดยไม่ได้?


1
ความหมายของการแทนที่อนุญาตให้มีการแปลงที่ชัดเจนและการเริ่มต้นโดยตรง
Kerrek SB

3
ตอนนี้operator[]ขึ้นอยู่กับtry_emplace. อาจเป็นมูลค่าการกล่าวขวัญinsert_or_assignเช่นกัน
FrankHB

@FrankHB ถ้าคุณ (หรือคนอื่น) เพิ่มคำตอบที่เป็นปัจจุบันฉันสามารถเปลี่ยนคำตอบที่ยอมรับได้
German Capuano

คำตอบ:


243

ในกรณีเฉพาะของแผนที่ตัวเลือกเก่ามีเพียงสองอย่าง: operator[]และinsert(รสชาติที่แตกต่างกันinsert) ดังนั้นฉันจะเริ่มอธิบายสิ่งเหล่านั้น

operator[]เป็นหาหรือเพิ่มผู้ประกอบการ มันจะพยายามค้นหาองค์ประกอบที่มีคีย์ที่กำหนดภายในแผนที่และหากมีอยู่ก็จะส่งคืนการอ้างอิงไปยังค่าที่เก็บไว้ หากไม่เป็นเช่นนั้นจะสร้างองค์ประกอบใหม่ที่แทรกเข้ามาพร้อมกับการเริ่มต้นเริ่มต้นและส่งคืนการอ้างอิงไปยังองค์ประกอบนั้น

insertฟังก์ชั่น (ในรสชาติองค์ประกอบเดียว) ยิงvalue_type( std::pair<const Key,Value>) จะใช้กุญแจ ( firstสมาชิก) และพยายามที่จะใส่ เนื่องจากstd::mapไม่อนุญาตให้มีการทำซ้ำหากมีองค์ประกอบที่มีอยู่มันจะไม่แทรกอะไรเลย

ความแตกต่างระหว่างสองคือoperator[]ความต้องการที่จะสามารถที่จะสร้างเริ่มต้นเริ่มต้นค่าและมันจึงใช้ไม่ได้สำหรับประเภทค่าที่ไม่สามารถเริ่มต้นเริ่มต้น ความแตกต่างที่สองระหว่างทั้งสองคือสิ่งที่เกิดขึ้นเมื่อมีองค์ประกอบที่มีคีย์ที่กำหนดอยู่แล้ว insertฟังก์ชั่นจะไม่ปรับเปลี่ยนสถานะของแผนที่ แต่แทนที่จะกลับ iterator เพื่อองค์ประกอบ (และfalseแสดงให้เห็นว่ามันก็ไม่ได้ใส่)

// assume m is std::map<int,int> already has an element with key 5 and value 0
m[5] = 10;                      // postcondition: m[5] == 10
m.insert(std::make_pair(5,15)); // m[5] is still 10

ในกรณีของinsertอาร์กิวเมนต์เป็นวัตถุvalue_typeซึ่งสามารถสร้างได้ในรูปแบบต่างๆ คุณสามารถสร้างได้โดยตรงด้วยประเภทที่เหมาะสมหรือส่งผ่านวัตถุใด ๆ ที่value_typeสามารถสร้างได้ซึ่งเป็นที่std::make_pairมาของการเล่นเนื่องจากช่วยให้สามารถสร้างได้ง่ายstd::pairวัตถุแม้ว่ามันอาจไม่ใช่สิ่งที่คุณต้องการ ...

ผลสุทธิของการเรียกต่อไปนี้คล้ายกัน :

K t; V u;
std::map<K,V> m;           // std::map<K,V>::value_type is std::pair<const K,V>

m.insert( std::pair<const K,V>(t,u) );      // 1
m.insert( std::map<K,V>::value_type(t,u) ); // 2
m.insert( std::make_pair(t,u) );            // 3

แต่ไม่เหมือนกันจริงๆ ... [1] และ [2] นั้นเทียบเท่ากันจริงๆ ในทั้งสองกรณีรหัสจะสร้างวัตถุชั่วคราวประเภทเดียวกัน ( std::pair<const K,V>) และส่งต่อไปยังinsertฟังก์ชัน insertฟังก์ชั่นจะสร้างโหนดที่เหมาะสมในต้นไม้ค้นหาแบบทวิภาคและคัดลอกvalue_typeส่วนหนึ่งมาจากการโต้แย้งโหนด ข้อดีของการใช้value_typeคือตรงvalue_typeเสมอคุณไม่สามารถพิมพ์ประเภทของอาร์กิวเมนต์ผิดได้! value_typestd::pair

ความแตกต่างอยู่ใน [3] ฟังก์ชันstd::make_pairนี้เป็นฟังก์ชันเทมเพลตที่จะสร้างไฟล์std::pair. ลายเซ็นคือ:

template <typename T, typename U>
std::pair<T,U> make_pair(T const & t, U const & u );

ฉันไม่ได้ตั้งใจให้อาร์กิวเมนต์ของเทมเพลตstd::make_pairเนื่องจากเป็นการใช้งานทั่วไป และความหมายก็คืออาร์กิวเมนต์แม่แบบจะอนุมานจากการโทรในกรณีนี้จะเป็นT==K,U==Vดังนั้นการเรียกstd::make_pairกลับจะส่งคืน a std::pair<K,V>(สังเกตว่าขาดหายไปconst) ลายเซ็นต้องการvalue_typeให้ใกล้เคียงแต่ไม่เหมือนกับค่าที่ส่งคืนจากการโทรไปยังstd::make_pairแต่ไม่เหมือนกันเป็นค่ากลับมาจากการเรียกร้องให้เนื่องจากมันอยู่ใกล้พอมันจะสร้างประเภทที่ถูกต้องชั่วคราวและคัดลอกเริ่มต้น สิ่งนี้จะถูกคัดลอกไปยังโหนดโดยสร้างสำเนาทั้งหมดสองชุด

สิ่งนี้สามารถแก้ไขได้โดยการระบุอาร์กิวเมนต์ของเทมเพลต:

m.insert( std::make_pair<const K,V>(t,u) );  // 4

แต่นั่นก็ยังเกิดข้อผิดพลาดได้ง่ายเช่นเดียวกับการพิมพ์ประเภทในกรณี [1] อย่างชัดเจน

ถึงจุดนี้เรามีวิธีการเรียกinsertที่แตกต่างกันซึ่งต้องสร้างจากvalue_typeภายนอกและสำเนาของวัตถุนั้นลงในคอนเทนเนอร์ หรือคุณสามารถใช้operator[]หากประเภทเป็นค่าเริ่มต้นที่สร้างได้และกำหนดได้ (โดยตั้งใจเน้นเฉพาะในm[k]=v) และต้องมีการเริ่มต้นเริ่มต้นของวัตถุหนึ่งและสำเนาค่าลงในวัตถุนั้น

ใน C ++ 11 กับแม่แบบ variadic และการส่งต่อที่สมบูรณ์แบบมีวิธีการใหม่ของการเพิ่มองค์ประกอบลงในภาชนะโดยวิธีการของemplacing (การสร้างในสถานที่) emplaceฟังก์ชั่นที่แตกต่างกันในภาชนะบรรจุที่ทำพื้นเดียวกัน: แทนที่จะได้รับแหล่งที่มาจากการที่จะคัดลอกลงในภาชนะที่ฟังก์ชั่นใช้พารามิเตอร์ที่จะถูกส่งต่อไปยังคอนสตรัคของวัตถุที่เก็บไว้ในภาชนะ

m.emplace(t,u);               // 5

ใน [5] std::pair<const K, V>ไม่ได้สร้างและส่งต่อไปยังemplaceแต่การอ้างอิงถึงtและuอ็อบเจ็กต์จะถูกส่งemplaceต่อไปยังตัวสร้างของvalue_typeอ็อบเจ็กต์ย่อยภายในโครงสร้างข้อมูล ในกรณีนี้จะไม่มีการทำสำเนาstd::pair<const K,V>เลยซึ่งเป็นข้อดีของemplaceทางเลือก C ++ 03 ในกรณีinsertนี้จะไม่แทนที่ค่าในแผนที่


คำถามที่น่าสนใจที่ฉันไม่เคยคิดมาก่อนคือemplaceจะนำไปใช้กับแผนที่ได้อย่างไรและนั่นไม่ใช่ปัญหาง่ายๆในกรณีทั่วไป


6
นี่เป็นคำตอบในคำตอบ แต่ map [] = val จะเขียนทับค่าก่อนหน้านี้หากมีอยู่
dk123

คำถามที่น่าสนใจกว่าในความหมายของฉันคือมันมีจุดประสงค์เพียงเล็กน้อย เนื่องจากคุณบันทึกสำเนาคู่ซึ่งเป็นสิ่งที่ดีเนื่องจากไม่มีสำเนาคู่หมายถึงไม่มีmapped_typeสำเนาไม่ได้ สิ่งที่เราต้องการคือการวางโครงสร้างของmapped_typeคู่และวางโครงสร้างคู่ในแผนที่ ดังนั้นstd::pair::emplaceฟังก์ชันและการสนับสนุนการส่งต่อmap::emplaceจึงหายไปทั้งคู่ ในรูปแบบปัจจุบันคุณยังคงต้องให้ mapped_type ที่สร้างขึ้นให้กับตัวสร้างคู่ซึ่งจะคัดลอกหนึ่งครั้ง มันดีกว่าสองเท่า แต่ก็ยังไม่ดี
v.oddou

ที่จริงฉันแก้ไขความคิดเห็นนั้นใน C ++ 11 มีตัวสร้างคู่แม่แบบที่ตอบสนองจุดประสงค์เดียวกันกับ emplace ในกรณีของการสร้างอาร์กิวเมนต์ และการสร้างแบบทีละชิ้นแบบแปลก ๆ ตามที่พวกเขาเรียกโดยใช้ทูเปิลในการส่งต่อข้อโต้แย้งดังนั้นเรายังคงสามารถส่งต่อได้อย่างสมบูรณ์
v.oddou

ดูเหมือนว่าจะมีข้อบกพร่องด้านประสิทธิภาพของการแทรกใน unordered_map และ map: link
Deqing

2
อาจเป็นการดีที่จะอัปเดตข้อมูลนี้ด้วยข้อมูลinsert_or_assignและtry_emplace(ทั้งจาก C ++ 17) ซึ่งช่วยเติมเต็มช่องว่างในการทำงานจากวิธีการที่มีอยู่
ShadowRanger

16

Emplace: ใช้ประโยชน์จากการอ้างอิง rvalue เพื่อใช้วัตถุจริงที่คุณสร้างไว้แล้ว ซึ่งหมายความว่าไม่มีการเรียกตัวสร้างการคัดลอกหรือย้ายซึ่งเหมาะสำหรับวัตถุขนาดใหญ่! O (บันทึก (N)) เวลา

แทรก: มีโอเวอร์โหลดสำหรับการอ้างอิง lvalue มาตรฐานและการอ้างอิง rvalue ตลอดจนตัววนซ้ำไปยังรายการองค์ประกอบที่จะแทรกและ "คำใบ้" เกี่ยวกับตำแหน่งที่องค์ประกอบนั้นอยู่ การใช้ตัววนซ้ำ "คำใบ้" สามารถทำให้การแทรกเวลาใช้เวลาลดลงเป็นเวลาที่แน่นอนมิฉะนั้นจะเป็นเวลา O (log (N))

ตัวดำเนินการ []: ตรวจสอบว่ามีวัตถุอยู่หรือไม่และแก้ไขการอ้างอิงไปยังวัตถุนี้หรือไม่หรือใช้คีย์และค่าที่ให้มาเพื่อเรียก make_pair บนวัตถุทั้งสองจากนั้นจะทำงานเช่นเดียวกับฟังก์ชันแทรก นี่คือเวลา O (log (N))

make_pair: ทำมากกว่าคู่

ไม่มี "ความจำเป็น" ในการเพิ่ม emplace ลงในมาตรฐาน ใน c ++ 11 ฉันเชื่อว่ามีการเพิ่มประเภท && ของการอ้างอิง สิ่งนี้ช่วยลดความจำเป็นในการย้ายความหมายและอนุญาตให้มีการเพิ่มประสิทธิภาพของการจัดการหน่วยความจำบางประเภท โดยเฉพาะอย่างยิ่งการอ้างอิง rvalue ตัวดำเนินการแทรก (value_type &&) ที่โอเวอร์โหลดจะไม่ใช้ประโยชน์จากความหมาย in_place ดังนั้นจึงมีประสิทธิภาพน้อยกว่ามาก แม้ว่าจะให้ความสามารถในการจัดการกับการอ้างอิง rvalue แต่ก็ไม่สนใจจุดประสงค์หลักของพวกเขาซึ่งอยู่ที่การสร้างวัตถุ


4
" ไม่มี" ความจำเป็น "ในการเพิ่ม emplace ลงในมาตรฐาน" นี่เป็นเท็จอย่างเห็นได้ชัด emplace()เป็นเพียงวิธีเดียวในการแทรกองค์ประกอบที่ไม่สามารถคัดลอกหรือย้ายได้ (& ใช่บางทีเพื่อให้มีประสิทธิภาพมากที่สุดในการแทรกตัวสร้างที่คัดลอกและย้ายตัวสร้างมีค่าใช้จ่ายมากกว่าการก่อสร้างถ้ามีสิ่งนั้นอยู่) ดูเหมือนว่าคุณจะคิดผิด: มันไม่เกี่ยวกับ " [การ] ประโยชน์จากการอ้างอิง rvalue เพื่อใช้วัตถุจริงที่คุณสร้างไว้แล้ว "; ไม่มีวัตถุที่ถูกสร้างขึ้นยังและคุณส่งต่อmapข้อโต้แย้งที่มันต้องการที่จะสร้างมันขึ้นมาภายในตัวเอง คุณไม่ได้สร้างวัตถุ
underscore_d

10

นอกเหนือจากโอกาสในการเพิ่มประสิทธิภาพและไวยากรณ์ที่ง่ายกว่าแล้วความแตกต่างที่สำคัญระหว่างการแทรกและการเคลื่อนย้ายก็คือโอกาสหลังนี้ทำให้เกิดการแปลงอย่างชัดเจน (นี่คือทั่วทั้งไลบรารีมาตรฐานไม่ใช่เฉพาะสำหรับแผนที่)

นี่คือตัวอย่างเพื่อสาธิต:

#include <vector>

struct foo
{
    explicit foo(int);
};

int main()
{
    std::vector<foo> v;

    v.emplace(v.end(), 10);      // Works
    //v.insert(v.end(), 10);     // Error, not explicit
    v.insert(v.end(), foo(10));  // Also works
}

นี่เป็นรายละเอียดที่เฉพาะเจาะจงมาก แต่เมื่อคุณจัดการกับกลุ่มของ Conversion ที่กำหนดโดยผู้ใช้คุณควรคำนึงถึงสิ่งนี้


ลองนึกภาพว่า foo ต้องการสอง ints ใน ctor แทนที่จะเป็นหนึ่ง คุณจะใช้การโทรนี้ได้ไหม v.emplace(v.end(), 10, 10); ... หรือตอนนี้คุณต้องใช้: v.emplace(v.end(), foo(10, 10) ); ?
Kaitain

ฉันไม่สามารถเข้าถึงคอมไพเลอร์ได้ในขณะนี้ แต่ฉันจะถือว่านั่นหมายความว่าทั้งสองเวอร์ชันจะใช้งานได้ ตัวอย่างเกือบทั้งหมดที่คุณเห็นเพื่อemplaceใช้ประโยชน์จากคลาสที่ใช้พารามิเตอร์เดียว IMO จะทำให้ลักษณะของไวยากรณ์ตัวแปรของ emplace เป็นข้อตกลงที่ดีชัดเจนยิ่งขึ้นหากใช้พารามิเตอร์หลายตัวในตัวอย่าง
Kaitain

10

รหัสต่อไปนี้อาจช่วยให้คุณเข้าใจ "แนวคิดภาพรวม" ว่า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

สังเกตว่า:

  1. 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แนะนำเทคนิคที่ละเอียดอ่อน แต่สำคัญ
  2. ใส่แต่ละfoo0, foo1และfoo2จำเป็นต้องมี 2 สายให้เป็นหนึ่งในFoo's ก่อสร้างคัดลอก / ย้ายและ 2 โทรไปFoo ' s destructor (อย่างที่ผมอธิบาย):

    ก. การแทรกfoo0และfoo1สร้างอ็อบเจ็กต์ชั่วคราว ( foo4และfoo6ตามลำดับ) ซึ่งตัวทำลายถูกเรียกใช้ทันทีหลังจากการแทรกเสร็จสิ้น นอกจากนี้Foos ภายในของ unordered_map (ซึ่งก็คือFoos 5 และ 7) ยังมีการเรียกตัวทำลายของมันเมื่อ unordered_map ถูกทำลาย

    ข. ในการแทรกก่อนfoo2อื่นเราได้สร้างวัตถุคู่ที่ไม่ใช่ชั่วคราว (เรียกว่าpair) อย่างชัดเจนซึ่งเรียกว่าตัวFooสร้างการคัดลอกบนfoo2(สร้างfoo8เป็นสมาชิกภายในของpair) จากนั้นเราinsert()แก้ไขคู่นี้ซึ่งส่งผลให้unordered_mapเรียกตัวสร้างสำเนาอีกครั้ง (เปิดfoo8) เพื่อสร้างสำเนาภายใน ( foo9) ของตัวเอง เช่นเดียวกับfoos 0 และ 1 ผลลัพธ์ที่ได้คือตัวทำลายสองตัวที่เรียกการแทรกนี้โดยมีข้อแตกต่างเพียงอย่างเดียวfoo8คือตัวทำลายของถูกเรียกก็ต่อเมื่อเราไปถึงจุดสิ้นสุดmain()แทนที่จะถูกเรียกทันทีหลังจากinsert()เสร็จ

  3. การแทนที่foo3ส่งผลให้มีการเรียกตัวสร้างสำเนา / ย้ายเพียง 1 รายการ (สร้างfoo10ภายในในunordered_map) และเพียง 1 การเรียกไปยังตัวFooทำลายของ (ฉันจะกลับไปในภายหลัง)

  4. สำหรับfoo11เราส่งจำนวนเต็ม 11 โดยตรงไปemplace(11, d)เพื่อที่unordered_mapจะเรียกตัวFoo(int)สร้างในขณะที่การดำเนินการอยู่ในemplace()เมธอด ซึ่งแตกต่างจากใน (2) และ (3) เราไม่จำเป็นต้องมีfooวัตถุก่อนออกเพื่อทำสิ่งนี้ด้วยซ้ำ ที่สำคัญสังเกตว่ามีการเรียกตัวFooสร้างเพียง 1 ครั้งเท่านั้น(ซึ่งสร้างขึ้นfoo11)

  5. จากนั้นเราจะผ่านจำนวนเต็ม 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) ด้านล่าง

  1. ต่อ: สาเหตุที่การเรียก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}});
//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สามารถมีผลสำคัญต่อจำนวนวัตถุที่ถูกคัดลอกย้ายสร้างและ / หรือทำลายรวมทั้งเมื่อสิ่งนี้เกิดขึ้นทั้งหมด

ข. ดูว่าจะเกิดอะไรขึ้นเมื่อคุณใช้คลาสคอนเทนเนอร์อื่น ๆ (เช่นstd::setหรือstd::unordered_multiset) แทนstd::unordered_mapแทน

ค. ตอนนี้ใช้Gooวัตถุ (เพียงสำเนาที่เปลี่ยนชื่อFoo) แทนintเป็นประเภทช่วงในunordered_map(เช่นใช้unordered_map<Foo, Goo>แทนunordered_map<Foo, int>) และดูจำนวนและตัวGooสร้างที่เรียกว่า (สปอยเลอร์: มีเอฟเฟกต์ แต่มันไม่ได้ดราม่ามาก)


0

ในแง่ของฟังก์ชันการทำงานหรือเอาต์พุตทั้งสองเหมือนกัน

สำหรับหน่วยความจำขนาดใหญ่ทั้งสองอ็อบเจ็กต์ emplace ได้รับการปรับให้เหมาะสมกับหน่วยความจำซึ่งไม่ใช้ตัวสร้างการคัดลอก

สำหรับคำอธิบายโดยละเอียด https://medium.com/@sandywits/all-about-emplace-in-c-71fd15e06e44

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.