เหตุใดจึงต้องใช้ std :: make_unique ใน C ++ 17


98

เท่าที่ฉันเข้าใจ C ++ 14 แนะนำstd::make_uniqueเนื่องจากไม่ได้ระบุลำดับการประเมินพารามิเตอร์สิ่งนี้ไม่ปลอดภัย:

f(std::unique_ptr<MyClass>(new MyClass(param)), g()); // Syntax A

(คำอธิบาย: หากการประเมินจัดสรรหน่วยความจำให้กับตัวชี้ดิบเป็นอันดับแรกการเรียกใช้g()และข้อยกเว้นจะเกิดขึ้นก่อนการstd::unique_ptrสร้างหน่วยความจำจะรั่วไหล)

การโทรstd::make_uniqueเป็นวิธีการ จำกัด ลำดับการโทรซึ่งทำให้สิ่งต่างๆปลอดภัย:

f(std::make_unique<MyClass>(param), g());             // Syntax B

ตั้งแต่นั้นมา, C ++ 17 ได้ชี้แจงเพื่อการประเมินผลที่ทำให้ไวยากรณ์ปลอดภัยเกินไปดังนั้นนี่คือคำถามของฉัน: คือยังคงมีเหตุผลที่จะใช้std::make_uniqueมากกว่าstd::unique_ptr's constructor ใน C ++ 17? คุณสามารถยกตัวอย่างได้หรือไม่?

ณ ตอนนี้เหตุผลเดียวที่ฉันนึกได้คืออนุญาตให้พิมพ์ได้MyClassเพียงครั้งเดียว (สมมติว่าคุณไม่จำเป็นต้องพึ่งพาความหลากหลายด้วยstd::unique_ptr<Base>(new Derived(param))) อย่างไรก็ตามนั่นดูเหมือนเป็นเหตุผลที่ค่อนข้างอ่อนแอโดยเฉพาะอย่างยิ่งเมื่อstd::make_uniqueไม่อนุญาตให้ระบุ deleter ในขณะที่ตัวstd::unique_ptrสร้างทำ

และเพื่อความชัดเจนฉันไม่สนับสนุนให้ลบออกstd::make_uniqueจาก Standard Library (อย่างน้อยก็เหมาะสมสำหรับความเข้ากันได้แบบย้อนหลัง) แต่ค่อนข้างสงสัยว่ายังมีสถานการณ์ที่ต้องการอย่างยิ่งstd::unique_ptr


4
อย่างไรก็ตามดูเหมือนว่าเป็นเหตุผลที่ค่อนข้างอ่อนแอ -> ทำไมถึงเป็นเหตุผลที่อ่อนแอ? ลดความซ้ำซ้อนของรหัสได้อย่างมีประสิทธิภาพ สำหรับ deleter คุณใช้ตัวลดขนาดแบบกำหนดเองบ่อยแค่ไหนเมื่อคุณใช้std::unique_ptr? ไม่ใช่การโต้แย้งmake_unique
llllllllll

2
ฉันบอกว่ามันเป็นเหตุผลที่อ่อนแอเพราะถ้าไม่มีstd::make_uniqueตั้งแต่แรกฉันไม่คิดว่านั่นจะมีเหตุผลเพียงพอที่จะเพิ่มลงใน STL โดยเฉพาะอย่างยิ่งเมื่อเป็นไวยากรณ์ที่แสดงออกน้อยกว่าการใช้ตัวสร้างไม่มาก
นิรันดร์

1
หากคุณมีโปรแกรมที่สร้างขึ้นใน c ++ 14 โดยใช้ make_unique คุณไม่ต้องการให้ฟังก์ชันถูกลบออกจาก stl หรือถ้าคุณต้องการให้เข้ากันได้แบบย้อนกลับ
เสิร์จ

2
@Serge นั่นเป็นจุดที่ดี แต่มันก็เป็นประเด็นที่นอกเหนือไปจากคำถาม ฉันจะแก้ไขให้ชัดเจนขึ้น
ชั่วนิรันดร์

1
@Eternal โปรดหยุดอ้างถึง C ++ Standard Library เป็น STL เนื่องจากไม่ถูกต้องและสร้างความสับสน ดูstackoverflow.com/questions/5205491/…
Marandil

คำตอบ:


77

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

อย่าลืมความสม่ำเสมอด้วย คุณควรใช้อย่างยิ่งmake_sharedดังนั้นการใช้จึงmake_uniqueเป็นธรรมชาติและเข้ากับรูปแบบ จากนั้นจึงเป็นเรื่องเล็กน้อยที่จะเปลี่ยนstd::make_unique<MyClass>(param)เป็นstd::make_shared<MyClass>(param)(หรือย้อนกลับ) โดยที่ไวยากรณ์ A ต้องการการเขียนซ้ำมากขึ้น


44
@reggaeguitar ถ้าฉันเห็นnewฉันต้องหยุดและคิดว่าตัวชี้นี้จะอยู่ได้นานแค่ไหน? ฉันจัดการอย่างถูกต้องหรือไม่? หากมีข้อยกเว้นการทำความสะอาดทุกอย่างถูกต้องหรือไม่? ฉันไม่อยากถามคำถามเหล่านั้นกับตัวเองเสียเวลากับมันและถ้าฉันไม่ใช้newฉันก็ไม่ต้องถามคำถามเหล่านั้น
NathanOliver

7
ลองนึกภาพคุณทำ grep newกว่าไฟล์ที่มาของโครงการทั้งหมดของคุณและไม่พบเดียว นี่จะไม่วิเศษเหรอ?
Sebastian Mach

5
ข้อได้เปรียบหลักของแนวทาง "อย่าใช้ใหม่" ซึ่งเป็นแนวทางง่ายๆดังนั้นจึงเป็นแนวทางที่ง่ายสำหรับนักพัฒนาที่มีประสบการณ์น้อยที่คุณอาจกำลังทำงานอยู่ ฉันไม่ได้ตระหนักในตอนแรก แต่นั่นมีค่าในตัวของมันเอง
นิรันดร์

@NathanOliver คุณจริงไม่อย่างจะต้องมีการใช้std::make_shared- จินตนาการกรณีที่วัตถุจัดสรรมีขนาดใหญ่และมีจำนวนมากstd::weak_ptrชี้ -s มัน: มันจะดีกว่าที่จะปล่อยให้วัตถุที่ถูกลบออกโดยเร็วที่สุดเท่าสุดท้ายที่ใช้ร่วมกัน ตัวชี้ถูกทำลายและอยู่ในพื้นที่ที่ใช้ร่วมกันเพียงเล็กน้อย
Dev Null

1
@NathanOliver คุณจะไม่ สิ่งที่ฉันกำลังพูดถึงคือข้อเสียของstd::make_shared stackoverflow.com/a/20895705/8414561ซึ่งหน่วยความจำที่ใช้ในการจัดเก็บวัตถุจะไม่สามารถปลดปล่อยได้จนกว่าสุดท้ายstd::weak_ptrจะหายไป (แม้ว่าทั้งหมดstd::shared_ptrจะชี้ไปที่มัน (และ ดังนั้นวัตถุเอง) ถูกทำลายไปแล้ว)
Dev Null

53

make_uniqueแตกต่างTจากT[]และT[N], unique_ptr(new ...)ไม่ได้

คุณสามารถรับพฤติกรรมที่ไม่ได้กำหนดได้อย่างง่ายดายโดยส่งตัวชี้ที่ถูกnew[]แก้ไขไปยัง a unique_ptr<T>หรือโดยการส่งตัวชี้ที่แก้ไขnewไปยังไฟล์unique_ptr<T[]>.


มันแย่กว่านั้น: มันไม่เพียง แต่ไม่สามารถแบนออกได้
Deduplicator

22

เหตุผลก็คือการมีรหัสที่สั้นกว่าโดยไม่ซ้ำกัน เปรียบเทียบ

f(std::unique_ptr<MyClass>(new MyClass(param)), g());
f(std::make_unique<MyClass>(param), g());

คุณประหยัดMyClass, newและวงเล็บ มีค่าใช้จ่ายเพียงหนึ่งตัวมากขึ้นในการทำในการเปรียบเทียบกับPTR


2
อย่างที่ฉันพูดในคำถามฉันเห็นว่ามีการพิมพ์น้อยลงโดยมีการกล่าวถึงเพียงครั้งเดียวMyClassแต่ฉันสงสัยว่ามีเหตุผลที่ดีกว่าที่จะใช้หรือไม่
Eternal

2
ในหลาย ๆ กรณีคำแนะนำการหักเงินจะช่วยกำจัด<MyClass>ส่วนในตัวแปรแรก
AnT

9
มีการกล่าวไว้แล้วในความคิดเห็นสำหรับคำตอบอื่น ๆ แต่ในขณะที่ c ++ 17 แนะนำการหักประเภทเทมเพลตสำหรับตัวสร้างในกรณีที่std::unique_ptrไม่อนุญาต มันเกี่ยวข้องกับการแยกแยะstd::unique_ptr<T>และstd::unique_ptr<T[]>
นิรันดร์

19

การใช้งานทุกครั้งnewจะต้องได้รับการตรวจสอบอย่างรอบคอบเป็นพิเศษเพื่อความถูกต้องตลอดอายุการใช้งาน มันถูกลบ? ครั้งเดียวเท่านั้น?

การใช้งานทุกครั้งmake_uniqueไม่ได้มีไว้สำหรับลักษณะพิเศษเหล่านั้น ตราบใดที่วัตถุที่เป็นเจ้าของมีอายุการใช้งาน "ถูกต้อง" มันจะทำให้ตัวชี้ที่ไม่ซ้ำกัน "ถูกต้อง" ซ้ำ ๆ

ตอนนี้มันเป็นความจริงที่unique_ptr<Foo>(new Foo())เหมือนกันทุกประการ1ถึงmake_unique<Foo>(); เพียงแค่ต้องการ "grep ซอร์สโค้ดของคุณที่ง่ายกว่าสำหรับการใช้งานทั้งหมดในnewการตรวจสอบ"


1เป็นเรื่องโกหกในกรณีทั่วไป การส่งต่อที่สมบูรณ์แบบไม่สมบูรณ์แบบ{}ค่าเริ่มต้นอาร์เรย์เป็นข้อยกเว้นทั้งหมด


ในทางเทคนิคunique_ptr<Foo>(new Foo)ไม่เหมือนกันเลยทีเดียวกับmake_unique<Foo>()... หลังทำnew Foo()แต่อย่างอื่นใช่
Barry

@barry true ตัวดำเนินการที่โอเวอร์โหลดใหม่เป็นไปได้
Yakk - Adam Nevraumont

@dedup คาถา C ++ 17 เหม็นอะไรนั่น?
Yakk - Adam Nevraumont

2
@Deduplicator ในขณะที่ c ++ 17 แนะนำการหักประเภทเทมเพลตสำหรับตัวสร้างในกรณีที่std::unique_ptrไม่อนุญาต หากเกี่ยวข้องกับการแยกแยะstd::unique_ptr<T>และstd::unique_ptr<T[]>
นิรันดร์

@ Yakk-AdamNevraumont ฉันไม่ได้หมายถึงการโอเวอร์โหลดใหม่ฉันแค่หมายถึง default-init เทียบกับ value-init
Barry

0

ตั้งแต่นั้นมา C ++ 17 ได้ชี้แจงลำดับการประเมินทำให้ Syntax A ปลอดภัยเช่นกัน

นั่นไม่ดีพอจริงๆ การใช้ข้อกำหนดทางเทคนิคที่เพิ่งเปิดตัวเมื่อเร็ว ๆ นี้เนื่องจากการรับประกันความปลอดภัยไม่ใช่แนวทางปฏิบัติที่เข้มงวดมาก:

  • อาจมีคนคอมไพล์โค้ดนี้ใน C ++ 14
  • คุณจะสนับสนุนให้ใช้ดิบnewที่อื่นเช่นโดยการคัดลอกวางตัวอย่างของคุณ
  • ตามที่ SM แนะนำเนื่องจากมีการทำซ้ำรหัสประเภทหนึ่งอาจได้รับการเปลี่ยนแปลงโดยที่อีกประเภทหนึ่งไม่ได้รับการเปลี่ยนแปลง
  • การรีแฟคเตอร์ IDE อัตโนมัติบางประเภทอาจย้ายไปที่newอื่น (ตกลงให้โอกาสไม่มากนัก)

โดยทั่วไปเป็นความคิดที่ดีที่รหัสของคุณจะเหมาะสม / แข็งแกร่ง / ถูกต้องชัดเจนโดยไม่ต้องอาศัยการวางภาษาโดยมองหาข้อกำหนดทางเทคนิคเล็กน้อยหรือคลุมเครือในมาตรฐาน

(นี่เป็นข้อโต้แย้งเดียวกับที่ฉันทำที่นี่เกี่ยวกับลำดับการทำลายทูเพิล)


-1

พิจารณาฟังก์ชัน void (std :: unique_ptr (new A ()), std :: unique_ptr (new B ())) {... }

สมมติว่า A () ใหม่สำเร็จ แต่ B () ใหม่แสดงข้อยกเว้น: คุณจับมันเพื่อกลับมาทำงานตามปกติของโปรแกรมของคุณ น่าเสียดายที่มาตรฐาน C ++ ไม่ต้องการให้อ็อบเจ็กต์ A ถูกทำลายและหน่วยความจำถูกยกเลิกการจัดสรร: หน่วยความจำรั่วไหลอย่างเงียบ ๆ และไม่มีวิธีใดที่จะทำความสะอาดได้ โดยการห่อ A และ B ไว้ใน std :: make_uniques คุณมั่นใจว่าจะไม่เกิดการรั่วไหล:

ฟังก์ชันโมฆะ (std :: make_unique (), std :: make_unique ()) {... } ประเด็นตรงนี้คือ std :: make_unique และ std :: make_unique เป็นอ็อบเจ็กต์ชั่วคราวและการล้างอ็อบเจ็กต์ชั่วคราวถูกระบุอย่างถูกต้องใน มาตรฐาน C ++: ตัวทำลายของพวกมันจะถูกกระตุ้นและหน่วยความจำจะถูกปลดปล่อย ดังนั้นหากทำได้ให้เลือกจัดสรรวัตถุโดยใช้ std :: make_unique และ std :: make_shared


4
ผู้เขียนระบุอย่างชัดเจนในคำถามที่ว่าใน C ++ 17 การรั่วไหลจะไม่เกิดขึ้นอีกต่อไป "ตั้งแต่นั้นเป็นต้นมา C ++ 17 ได้ชี้แจงลำดับการประเมินทำให้ Syntax A ปลอดภัยเช่นกันคำถามของฉันมีดังนี้: (... )" คุณไม่ได้ตอบคำถามของเขา
R2RT
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.