ทำไมฉันถึงต้องใช้ push_back แทน emplace_back


232

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

ฉันต้องใช้เหตุผลpush_backอะไร

คำตอบ:


162

ฉันคิดถึงคำถามนี้ในช่วงสี่ปีที่ผ่านมา ฉันได้ข้อสรุปว่าคำอธิบายส่วนใหญ่เกี่ยวpush_backกับ vs. emplace_backคิดถึงภาพเต็ม

ปีที่แล้วผมได้นำเสนอที่ C ++ ตอนนี้ในประเภทหักใน C ฉันเริ่มพูดถึงpush_backvs. emplace_backที่ 13:49 แต่มีข้อมูลที่เป็นประโยชน์ที่ให้หลักฐานสนับสนุนก่อนหน้านั้น

ความแตกต่างหลักที่แท้จริงเกี่ยวข้องกับตัวสร้างโดยนัยกับตัวสร้างที่ชัดเจน พิจารณากรณีที่เรามีอาร์กิวเมนต์เดียวที่เราต้องการส่งต่อไปหรือpush_backemplace_back

std::vector<T> v;
v.push_back(x);
v.emplace_back(x);

หลังจากคอมไพเลอร์การปรับให้เหมาะสมของคุณได้รับสิ่งนี้แล้วไม่มีความแตกต่างระหว่างสองข้อความนี้ในแง่ของโค้ดที่สร้างขึ้น ภูมิปัญญาดั้งเดิมคือว่าpush_backจะสร้างวัตถุชั่วคราวซึ่งจะถูกย้ายเข้าไปvในขณะที่emplace_backจะส่งต่อการโต้เถียงตามและสร้างมันโดยตรงในสถานที่โดยไม่มีการคัดลอกหรือย้าย สิ่งนี้อาจเป็นจริงตามรหัสที่เขียนในไลบรารีมาตรฐาน แต่มันทำให้เข้าใจผิดว่างานเพิ่มประสิทธิภาพของคอมไพเลอร์คือการสร้างรหัสที่คุณเขียน การเพิ่มประสิทธิภาพของคอมไพเลอร์คือการสร้างรหัสที่คุณจะเขียนถ้าคุณเป็นผู้เชี่ยวชาญในการปรับแต่งเฉพาะแพลตฟอร์มและไม่สนใจเกี่ยวกับความสามารถในการบำรุงรักษาเพียงประสิทธิภาพ

ความแตกต่างที่เกิดขึ้นจริงระหว่างข้อความทั้งสองนี้คือยิ่งมีพลังมากขึ้นemplace_backจะเรียกใช้ตัวสร้างประเภทใดก็ได้ในขณะที่ความระมัดระวังยิ่งกว่าpush_backจะเรียกเฉพาะตัวสร้างที่มีนัยเท่านั้น Constructor โดยปริยายน่าจะปลอดภัย หากคุณสามารถสร้าง a Uจากโดยปริยายได้Tคุณกำลังบอกว่าUสามารถเก็บข้อมูลทั้งหมดไว้ได้Tโดยไม่มีการสูญเสีย มีความปลอดภัยในทุกสถานการณ์ที่จะผ่านTและไม่มีใครจะรังเกียจถ้าคุณทำให้มันเป็นUแทน เป็นตัวอย่างที่ดีของตัวสร้างโดยนัยคือการแปลงจากไปstd::uint32_t std::uint64_tเป็นตัวอย่างที่ไม่ดีของการแปลงนัยคือการdoublestd::uint8_t

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

ตัวอย่าง

std::vector<std::unique_ptr<T>> v;
T a;
v.emplace_back(std::addressof(a)); // compiles
v.push_back(std::addressof(a)); // fails to compile

std::unique_ptr<T>T *มีคอนสตรัคอย่างชัดเจนจาก เพราะemplace_backสามารถเรียกคอนสตรัคเตอร์ที่ชัดเจนผ่านการคอมไพล์พอยน์เตอร์ที่ไม่ได้เป็นเจ้าของได้ดี อย่างไรก็ตามเมื่อvออกไปนอกขอบเขต destructor จะพยายามโทรหาdeleteตัวชี้นั้นซึ่งไม่ได้ถูกจัดสรรโดยnewเพราะมันเป็นเพียงวัตถุสแต็ก สิ่งนี้นำไปสู่พฤติกรรมที่ไม่ได้กำหนด

นี่ไม่ใช่แค่รหัสที่คิดค้น นี่เป็นข้อผิดพลาดการผลิตจริงที่ฉันพบ รหัสคือstd::vector<T *>แต่มันเป็นเจ้าของเนื้อหา ในฐานะที่เป็นส่วนหนึ่งของการโยกย้ายไปยัง C ++ 11 ผมอย่างถูกต้องเปลี่ยนแปลงT *เพื่อstd::unique_ptr<T>ที่จะแสดงให้เห็นว่าเวกเตอร์ที่เป็นเจ้าของหน่วยความจำ แต่ผมก็ basing การเปลี่ยนแปลงเหล่านี้ออกจากความเข้าใจของฉันในปี 2012 ในระหว่างที่ผมคิดว่า "ทุกอย่างไม่ emplace_back push_back สามารถทำและอื่น ๆ ดังนั้นทำไมฉันจะเคยใช้ push_back?" ดังนั้นผมก็เปลี่ยนไปpush_backemplace_back

หากฉันทิ้งรหัสไว้เป็นการใช้ที่ปลอดภัยกว่าpush_backฉันจะจับข้อผิดพลาดอันยาวนานนี้ได้ทันทีและจะถูกมองว่าเป็นความสำเร็จในการอัปเกรดเป็น C ++ 11 แต่ฉันปิดบังข้อบกพร่องและไม่พบมันจนกว่าจะถึงเดือนต่อมา


มันจะช่วยถ้าคุณสามารถอธิบายรายละเอียดเกี่ยวกับสิ่งที่เกิดขึ้นในตัวอย่างของคุณและทำไมมันผิด
eddi

4
@eddi: ผมเพิ่มส่วนนี้อธิบาย: มีคอนสตรัคอย่างชัดเจนจากstd::unique_ptr<T> T *เพราะemplace_backสามารถเรียกคอนสตรัคเตอร์อย่างชัดเจนผ่านคอมไพล์ที่ไม่ใช่เจ้าของได้ อย่างไรก็ตามเมื่อvออกไปนอกขอบเขต destructor จะพยายามโทรหาdeleteตัวชี้นั้นซึ่งไม่ได้ถูกจัดสรรnewเพราะมันเป็นเพียงวัตถุสแต็ก สิ่งนี้นำไปสู่พฤติกรรมที่ไม่ได้กำหนด
David Stone

ขอขอบคุณที่โพสต์สิ่งนี้ ฉันไม่ทราบเกี่ยวกับมันเมื่อฉันเขียนคำตอบของฉัน แต่ตอนนี้ฉันหวังว่าฉันจะเขียนมันเองเมื่อฉันเรียนรู้มันในภายหลัง :) ฉันต้องการที่จะตบคนที่เปลี่ยนไปใช้คุณสมบัติใหม่เพื่อทำสิ่งที่ทันสมัยที่สุดที่พวกเขาสามารถหาได้ . พวกผู้คนใช้ C ++ ก่อน C ++ 11 ด้วยและไม่ใช่ทุกอย่างที่เป็นปัญหา หากคุณไม่ทราบว่าทำไมคุณกำลังใช้คุณลักษณะที่ไม่ได้ใช้มัน ดีใจที่คุณโพสต์สิ่งนี้และฉันหวังว่ามันจะได้รับ upvotes มากขึ้น +1
user541686

1
@CaptainJacksparrow: ดูเหมือนว่าฉันพูดโดยนัยและชัดเจนที่ฉันหมายถึงพวกเขา ส่วนไหนที่คุณสับสน
David Stone

4
@CaptainJacksparrow: explicitนวกรรมิกเป็นตัวสร้างที่มีคำหลักที่explicitใช้กับมัน ตัวสร้าง "โดยนัย" เป็นตัวสร้างใด ๆ ที่ไม่มีคำหลักนั้น ในกรณีของคอนstd::unique_ptrสตรัคT *เตอร์ผู้ดำเนินการstd::unique_ptrเขียนคอนสตรัคเตอร์ แต่ปัญหาที่นี่คือผู้ใช้ประเภทนั้นเรียกว่าemplace_backซึ่งเรียกว่านวกรรมิกชัดเจน ถ้าเป็นpush_backเช่นนั้นแทนที่จะเรียกว่าคอนสตรัคเตอร์มันจะต้องอาศัยการแปลงโดยปริยายซึ่งสามารถเรียกคอนสตรัคเตอร์โดยปริยายได้เท่านั้น
David Stone

117

push_backอนุญาตให้ใช้การกำหนดค่าเริ่มต้นเสมอซึ่งฉันชอบมาก ตัวอย่างเช่น

struct aggregate {
    int foo;
    int bar;
};

std::vector<aggregate> v;
v.push_back({ 42, 121 });

ในทางกลับกันv.emplace_back({ 42, 121 });จะไม่ทำงาน


58
โปรดทราบว่าสิ่งนี้ใช้ได้เฉพาะกับการเริ่มต้นรวมและการเริ่มต้นรายการเริ่มต้น หากคุณต้องการใช้{}ไวยากรณ์เพื่อเรียกคอนสตรัคเตอร์จริงคุณสามารถลบ{}และใช้งานemplace_backได้
Nicol Bolas

ใบ้เวลาคำถาม: ดังนั้น emplace_back จึงไม่สามารถใช้กับเวกเตอร์ของ struct ได้เลย? หรือไม่ใช้รูปแบบนี้โดยใช้ตัวอักษร {42,121}
Phil H

1
@LucDanton: ที่ผมกล่าวว่าจะใช้กับการรวมและการเริ่มต้นรายการเริ่มต้น คุณสามารถใช้{}ไวยากรณ์เพื่อโทรก่อสร้างจริง คุณสามารถให้aggregateนวกรรมิกที่ใช้จำนวนเต็ม 2 จำนวนและตัวสร้างนี้จะถูกเรียกเมื่อใช้{}ไวยากรณ์ ประเด็นก็คือว่าถ้าคุณพยายามที่จะเรียกตัวสร้างemplace_backจะดีกว่าเพราะมันเรียกตัวสร้างในสถานที่ ดังนั้นจึงไม่ต้องการประเภทที่จะคัดลอกได้
Nicol Bolas

11
สิ่งนี้ถูกมองว่าเป็นข้อบกพร่องในมาตรฐานและได้รับการแก้ไขแล้ว ดูcplusplus.github.io/LWG/lwg-active.html#2089
David Stone

3
@DavidStone หากได้รับการแก้ไขแล้วจะยังคงไม่อยู่ในรายการ "ใช้งาน" ... ไม่ได้? ดูเหมือนว่าจะยังคงเป็นปัญหาที่ค้างอยู่ การอัปเดตล่าสุดที่มุ่งหน้าไปยัง " [2018-08-23 การประมวลผลปัญหาบาตาเวีย] " กล่าวว่า " P0960 (ขณะนี้อยู่ในระหว่างการบิน) ควรแก้ไขปัญหานี้ " และฉันยังคงไม่สามารถคอมไพล์โค้ดที่พยายามemplaceรวมกันโดยไม่ต้องเขียน . ยังไม่มีความชัดเจนในจุดนี้ว่าจะถือว่าเป็นข้อบกพร่องหรือไม่และมีสิทธิ์ในการ backporting หรือว่าผู้ใช้ C ++ <20 จะยังคงอยู่ใน SoL หรือไม่
underscore_d

80

ความเข้ากันได้ย้อนหลังกับคอมไพเลอร์ pre-C ++ 11


21
นั่นน่าจะเป็นคำสาปของ C ++ เราได้รับฟีเจอร์เจ๋ง ๆ มากมายในแต่ละรุ่นใหม่ แต่มี บริษัท จำนวนมากติดอยู่ในการใช้เวอร์ชั่นเก่าเพื่อความเข้ากันได้หรือการใช้คุณลักษณะบางอย่างที่ท้อแท้
Dan Albert

6
@ Mehrdad: ทำไมชำระเพียงพอเมื่อคุณสามารถมีดี ฉันแน่ใจว่าจะไม่ต้องการเขียนโปรแกรมในร้องไห้สะอึกสะอื้นแม้ว่ามันจะเพียงพอ ไม่ได้บอกว่าเป็นกรณีของตัวอย่างนี้โดยเฉพาะ แต่เป็นคนที่ใช้เวลาส่วนใหญ่ในการเขียนโปรแกรมใน C89 เพื่อความเข้ากันได้มันเป็นปัญหาที่แท้จริง
Dan Albert

3
ฉันไม่คิดว่านี่เป็นคำตอบสำหรับคำถามจริงๆ สำหรับฉันเขาขอใช้กรณีที่push_backเป็นที่นิยม
นายบอย

3
@ Mr.Boy: มันจะดีกว่าเมื่อคุณต้องการที่จะย้อนกลับเข้ากันได้กับคอมไพเลอร์ pre-C ++ 11 นั่นไม่ชัดเจนในคำตอบของฉัน?
user541686

6
นี้มีอากาศความสนใจมากกว่าที่ผมเคยคาดว่าเพื่อให้ทุกท่านที่อ่านข้อความนี้emplace_backคือไม่ได้เป็นรุ่นที่ "ยิ่งใหญ่" push_backของ มันเป็นรุ่นที่อาจเป็นอันตรายได้ อ่านคำตอบอื่น ๆ
user541686

68

การใช้งานไลบรารีบางส่วนของ emplace_back ไม่ได้ทำงานตามที่ระบุในมาตรฐาน C ++ รวมถึงรุ่นที่จัดส่งกับ Visual Studio 2012, 2013 และ 2015

เพื่อรองรับข้อบกพร่องของคอมไพเลอร์ที่รู้จักให้ใช้std::vector::push_back()ถ้าพารามิเตอร์อ้างอิงตัววนซ้ำหรือวัตถุอื่น ๆ ซึ่งจะไม่ถูกต้องหลังจากการโทร

std::vector<int> v;
v.emplace_back(123);
v.emplace_back(v[0]); // Produces incorrect results in some compilers

ในคอมไพเลอร์ตัวเดียว v มีค่า 123 และ 21 แทนค่าคาดหวัง 123 และ 123 เนื่องจากนี่เป็นความจริงที่ว่าการเรียกครั้งที่ 2 เพื่อให้emplace_backผลลัพธ์มีขนาดที่จุดใดv[0]ไม่ถูกต้อง

การใช้งานโค้ดด้านบนจะใช้งานpush_back()แทนemplace_back()ดังนี้:

std::vector<int> v;
v.emplace_back(123);
v.push_back(v[0]);

หมายเหตุ: การใช้เวกเตอร์ของ ints มีวัตถุประสงค์เพื่อสาธิต ฉันค้นพบปัญหานี้ด้วยคลาสที่ซับซ้อนมากขึ้นซึ่งรวมถึงตัวแปรสมาชิกที่จัดสรรแบบไดนามิกและการเรียกร้องให้emplace_back()เกิดความผิดพลาดอย่างหนัก


รอ. สิ่งนี้ดูเหมือนจะเป็นบั๊ก วิธีการอาจpush_backจะแตกต่างกันในกรณีนี้หรือไม่?
balki

12
การเรียกไปยัง emplace_back () ใช้การส่งต่อที่สมบูรณ์แบบเพื่อดำเนินการก่อสร้างในสถานที่และเนื่องจาก v [0] ดังกล่าวไม่ได้รับการประเมินจนกระทั่งหลังจากที่เวกเตอร์ถูกปรับขนาดแล้ว (ที่จุด v [0] ไม่ถูกต้อง) push_back สร้างอิลิเมนต์ใหม่และคัดลอก / ย้ายอิลิเมนต์ตามต้องการและ v [0] จะถูกประเมินก่อนการจัดสรรใหม่
Marc

1
@ Marc: รับประกันโดยมาตรฐานที่ emplace_back ใช้งานได้แม้กับองค์ประกอบที่อยู่ในช่วง
David Stone

1
@DavidStone: โปรดให้การอ้างอิงถึงมาตรฐานการทำงานนี้ที่รับประกันได้หรือไม่? ทั้งสองวิธี Visual Studio 2012 และ 2015 แสดงพฤติกรรมที่ไม่ถูกต้อง
Marc

4
@cameino: emplace_back มีอยู่เพื่อชะลอการประเมินพารามิเตอร์เพื่อลดการคัดลอกที่ไม่จำเป็น พฤติกรรมไม่ได้กำหนดหรือข้อผิดพลาดคอมไพเลอร์ (รอการวิเคราะห์มาตรฐาน) ฉันเพิ่งรันการทดสอบเดียวกันกับ Visual Studio 2015 และได้ 123,3 ภายใต้ Release x64, 123,40 ภายใต้ Release Win32 และ 123, -572662307 ภายใต้ Debug x64 และ Debug Win32
Marc
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.