เราควรส่ง shared_ptr โดยอ้างอิงหรือตามค่าหรือไม่


270

เมื่อฟังก์ชั่นใช้shared_ptr(จากบูสต์หรือ C ++ 11 STL) คุณจะผ่านมัน:

  • โดยอ้างอิง const: void foo(const shared_ptr<T>& p)

  • หรือโดยค่า: void foo(shared_ptr<T> p)?

ฉันชอบวิธีแรกเพราะฉันสงสัยว่ามันจะเร็วกว่า แต่สิ่งนี้คุ้มหรือไม่

คุณกรุณาให้เหตุผลในการเลือกของคุณหรือถ้าเป็นเช่นนั้นทำไมคุณคิดว่ามันไม่สำคัญ


14
ปัญหาคือสิ่งนั้นไม่เท่ากัน รุ่นอ้างอิงส่งเสียงร้อง "ฉันจะใช้นามแฝงshared_ptrและฉันสามารถเปลี่ยนได้ถ้าต้องการ" ในขณะที่รุ่นค่าแจ้งว่า "ฉันจะคัดลอกของคุณshared_ptrดังนั้นในขณะที่ฉันสามารถเปลี่ยนแปลงได้คุณจะไม่มีทางรู้ ) พารามิเตอร์ const-reference คือทางออกที่แท้จริงซึ่งบอกว่า "ฉันจะใช้นามแฝงบางอย่างshared_ptrและฉันสัญญาว่าจะไม่เปลี่ยนแปลง" (ซึ่งคล้ายกับ semantics ตามค่ามาก!)
GManNickG

2
เฮ้ฉันจะสนใจในความคิดของพวกคุณเกี่ยวกับการกลับมาshared_ptrสมาชิกชั้นเรียน คุณทำโดย const-refs หรือไม่
Johannes Schaub - litb

ความเป็นไปได้ที่สามคือการใช้ std :: move () ด้วย C ++ 0x, นี่เป็นการแลกเปลี่ยนทั้ง shared_ptr
Tomaka17

@ Johannes: ฉันจะส่งกลับโดยอ้างอิง const เพียงเพื่อหลีกเลี่ยงการคัดลอก / อ้างอิงนับ จากนั้นอีกครั้งฉันกลับสมาชิกทั้งหมดโดยอ้างอิง const เว้นแต่พวกเขาจะดั้งเดิม
GManNickG

สำเนาที่เป็นไปได้ของC ++ - คำถามผ่านตัวชี้
kennytm

คำตอบ:


229

คำถามนี้ได้รับการกล่าวถึงและตอบโดยสกอตต์, อังเดรและสมุนไพรในระหว่างการขอให้เราสิ่งใดเซสชั่นที่C ++ 2011 ดูจาก 04:34 ในshared_ptrประสิทธิภาพการทำงานและความถูกต้อง

ในเวลาสั้น ๆไม่มีเหตุผลที่จะส่งผ่านตามค่าเว้นแต่ว่าเป้าหมายคือการแบ่งปันความเป็นเจ้าของวัตถุ (เช่นระหว่างโครงสร้างข้อมูลที่แตกต่างกันหรือระหว่างหัวข้อที่แตกต่างกัน)

ยกเว้นว่าคุณสามารถย้ายปรับให้เหมาะสมตามที่อธิบายโดย Scott Meyers ในวิดีโอพูดคุยที่ลิงก์ข้างต้น แต่เกี่ยวข้องกับ C ++ เวอร์ชันจริงที่คุณสามารถใช้ได้

การอัปเดตครั้งสำคัญของการสนทนานี้เกิดขึ้นในช่วงแผงการโต้ตอบของGoingNative 2012 : ถามเราได้เลย ซึ่งมีมูลค่าการดูโดยเฉพาะอย่างยิ่งจาก22:50


5
แต่ดังที่แสดงไว้ที่นี่มันถูกกว่าที่จะผ่านค่า: stackoverflow.com/a/12002668/128384ไม่ควรถูกนำมาพิจารณาด้วยเช่นกัน (อย่างน้อยสำหรับอาร์กิวเมนต์ตัวสร้างและอื่น ๆ ที่ shared_ptr จะเป็นสมาชิกของ ห้องเรียน)?
stijn

2
@stijn ใช่และไม่ใช่ คำถามและคำตอบที่คุณชี้ไม่สมบูรณ์เว้นแต่จะระบุเวอร์ชันมาตรฐาน C ++ ที่ชัดเจน มันง่ายมากที่จะแพร่กระจายกฎทั่วไปที่ไม่เคยมีมาเสมอซึ่งทำให้เข้าใจผิด เว้นแต่ผู้อ่านจะใช้เวลาทำความคุ้นเคยกับบทความและการอ้างอิงของ David Abrahams หรือพิจารณาวันที่โพสต์เทียบกับมาตรฐาน C ++ ปัจจุบัน ดังนั้นทั้งคำตอบฉันและคนที่คุณชี้ว่าถูกต้องเมื่อถึงเวลาโพสต์
mloskot

1
" เว้นแต่จะมีหลายเธรด " ไม่ MT ไม่ได้พิเศษ
curiousguy

3
ฉันไปงานเลี้ยงช้าสุด แต่เหตุผลที่ฉันต้องการส่ง shared_ptr ตามค่าคือมันทำให้โค้ดสั้นและสวยกว่า อย่างจริงจัง. Value*สั้นและอ่านง่าย แต่ก็ไม่ดีดังนั้นตอนนี้โค้ดของฉันก็เต็มconst shared_ptr<Value>&และมันอ่านได้น้อยกว่าและเป็นระเบียบเรียบร้อยน้อยลง สิ่งที่เคยเป็นvoid Function(Value* v1, Value* v2, Value* v3)อยู่ตอนนี้void Function(const shared_ptr<Value>& v1, const shared_ptr<Value>& v2, const shared_ptr<Value>& v3)และผู้คนก็โอเคกับเรื่องนี้?
อเล็กซ์

7
@Alex การปฏิบัติทั่วไปคือการสร้างนามแฝง (typedefs) ทันทีหลังเลิกเรียน สำหรับตัวอย่างของคุณ: class Value {...}; using ValuePtr = std::shared_ptr<Value>;จากนั้นฟังก์ชั่นของคุณจะง่ายขึ้น: void Function(const ValuePtr& v1, const ValuePtr& v2, const ValuePtr& v3)และคุณจะได้รับประสิทธิภาพสูงสุด นั่นเป็นเหตุผลที่คุณใช้ C ++ ใช่ไหม :)
4LegsDrivenCat

92

นี่คือการนำของHerb Sutter

คำแนะนำ: อย่าส่งตัวชี้สมาร์ทเป็นพารามิเตอร์ฟังก์ชั่นเว้นแต่คุณต้องการใช้หรือจัดการตัวชี้สมาร์ทตัวเองเช่นเพื่อแบ่งปันหรือโอนความเป็นเจ้าของ

คำแนะนำ: แสดงว่าฟังก์ชั่นจะจัดเก็บและแบ่งปันความเป็นเจ้าของของวัตถุฮีปโดยใช้พารามิเตอร์ shared_ptr

คำแนะนำ: ใช้พารามิเตอร์ที่ไม่ใช่แบบคงที่ shared_ptr & เท่านั้นเพื่อแก้ไข shared_ptr ใช้ const shared_ptr & เป็นพารามิเตอร์เฉพาะในกรณีที่คุณไม่แน่ใจว่าคุณจะคัดลอกและแบ่งปันความเป็นเจ้าของหรือไม่ มิฉะนั้นให้ใช้วิดเจ็ต * แทน (หรือหากไม่ใช่โมฆะวิดเจ็ต &)


3
ขอบคุณสำหรับลิงค์ไปยังซัทเทอร์ มันเป็นบทความที่ยอดเยี่ยม ฉันไม่เห็นด้วยกับเขาในวิดเจ็ต * เลือก <widget &> ตัวเลือกถ้ามี C ++ 14 วิดเจ็ต * คลุมเครือเกินไปจากรหัสเก่า
บาร์

3
+1 สำหรับรวมถึง widget * และ widget & เป็นไปได้ เพียงเพื่อทำอย่างละเอียดผ่าน widget * หรือ widget & อาจเป็นตัวเลือกที่ดีที่สุดเมื่อฟังก์ชั่นไม่ได้ตรวจสอบ / แก้ไขวัตถุตัวชี้ อินเทอร์เฟซนั้นกว้างกว่าทั่วไปเนื่องจากมันไม่จำเป็นต้องมีประเภทพอยน์เตอร์ที่เฉพาะเจาะจงและปัญหาประสิทธิภาพการทำงานของจำนวนการอ้างอิง shared_ptr นั้นถูกหลบ
tgnottingham

4
ฉันคิดว่านี่ควรเป็นคำตอบที่ได้รับการยอมรับในวันนี้เพราะแนวทางที่สอง มันทำให้คำตอบที่ยอมรับในปัจจุบันเป็นโมฆะอย่างชัดเจนว่า: ไม่มีเหตุผลที่จะต้องผ่านคุณค่า
mbrt

62

ส่วนตัวฉันจะใช้การconstอ้างอิง ไม่จำเป็นต้องเพิ่มจำนวนการอ้างอิงเพียงเพื่อลดลงอีกครั้งเพื่อประโยชน์ของการเรียกใช้ฟังก์ชัน


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

@Danvil: คำนึงถึงวิธีการshared_ptrทำงานข้อเสียเดียวที่เป็นไปได้ที่จะไม่ผ่านการอ้างอิงคือการสูญเสียประสิทธิภาพเล็กน้อย มีสองสาเหตุที่นี่ a) คุณสมบัติการกำหนดนามแฝงตัวชี้หมายถึงพอยน์เตอร์ของข้อมูลที่มีค่ารวมทั้งตัวนับ (อาจเป็น 2 สำหรับการอ้างอิงอ่อน) ถูกคัดลอกดังนั้นจึงมีราคาแพงกว่าเล็กน้อยในการคัดลอกรอบข้อมูล b) การนับการอ้างอิงปรมาณูช้ากว่ารหัสเพิ่ม / ลดเก่าธรรมดาเล็กน้อยเล็กน้อย แต่จำเป็นต้องมีเพื่อความปลอดภัยของเธรด นอกเหนือจากนั้นทั้งสองวิธียังเหมือนกันสำหรับจุดประสงค์และจุดประสงค์ส่วนใหญ่
Evan Teran

37

ผ่านconstการอ้างอิงมันเร็วกว่า หากคุณต้องการเก็บมันพูดในภาชนะบางส่วนอ้างอิง การนับจะเพิ่มขึ้นอัตโนมัติอย่างน่าอัศจรรย์โดยการทำสำเนา


4
Downvote เป็นความคิดเห็นโดยไม่มีตัวเลขใด ๆ สำรอง
kwesolowski

22

ฉันวิ่งรหัสด้านล่างอีกครั้งกับfooการshared_ptrโดยconst&และอีกครั้งกับfooการshared_ptrตามมูลค่า

void foo(const std::shared_ptr<int>& p)
{
    static int x = 0;
    *p = ++x;
}

int main()
{
    auto p = std::make_shared<int>();
    auto start = clock();
    for (int i = 0; i < 10000000; ++i)
    {
        foo(p);
    }    
    std::cout << "Took " << clock() - start << " ms" << std::endl;
}

ใช้ VS2015, x86 release build บนตัวประมวลผล intel core 2 quad (2.4GHz) ของฉัน

const shared_ptr&     - 10ms  
shared_ptr            - 281ms 

รุ่นที่คัดลอกตามค่าเป็นลำดับความสำคัญช้ากว่า
หากคุณกำลังเรียกใช้ฟังก์ชันพร้อมกันจากเธรดปัจจุบันให้เลือกconst&เวอร์ชัน


1
คุณสามารถพูดได้ว่าคอมไพเลอร์แพลตฟอร์มและการตั้งค่าการเพิ่มประสิทธิภาพที่คุณใช้
คาร์ลตัน

ฉันใช้ debug build ของ vs2015 อัพเดตคำตอบเพื่อใช้ build build ตอนนี้
tcb

1
ฉันอยากรู้ว่าเมื่อเปิดการปรับให้เหมาะสมคุณจะได้รับผลลัพธ์เดียวกันกับทั้งคู่
Elliot Woods

2
การเพิ่มประสิทธิภาพไม่ได้ช่วยอะไรมาก ปัญหาคือการช่วงชิงการล็อกบนจำนวนการอ้างอิงบนสำเนา
Alex

1
นั่นไม่ใช่ประเด็น. เช่นfoo()ฟังก์ชั่นจะไม่ได้รับเป็นตัวชี้ที่ใช้ร่วมกันในสถานที่แรกเพราะไม่ได้ใช้วัตถุนี้มันควรจะยอมรับint&และทำp = ++x;เรียกจากfoo(*p); main()ฟังก์ชั่นยอมรับวัตถุตัวชี้สมาร์ทเมื่อมันต้องการทำอะไรกับมันและส่วนใหญ่แล้วสิ่งที่คุณต้องทำคือการย้ายมัน ( std::move()) ไปยังที่อื่นดังนั้นพารามิเตอร์ by-value ไม่มีค่าใช้จ่าย
eepp

15

ตั้งแต่ C ++ 11 คุณควรรับมันด้วยค่าที่มากกว่า &บ่อยกว่าที่คุณคิด

หากคุณกำลังใช้ std :: shared_ptr (แทนที่จะเป็นประเภท T พื้นฐาน) แสดงว่าคุณกำลังทำเช่นนั้นเพราะคุณต้องการทำอะไรกับมัน

หากคุณต้องการคัดลอกไปที่ไหนสักแห่งมันเหมาะสมกว่าที่จะคัดลอกและ std :: ย้ายมันภายในแทนที่จะใช้มันโดย const & แล้วคัดลอกในภายหลัง นี่เป็นเพราะคุณอนุญาตให้ผู้เรียกใช้ตัวเลือกในการเปิด std :: ย้าย shared_ptr เมื่อเรียกใช้งานฟังก์ชั่นของคุณดังนั้นการบันทึกชุดของการดำเนินการที่เพิ่มขึ้นและลดลง หรือไม่. นั่นคือผู้โทรของฟังก์ชั่นสามารถตัดสินใจได้ว่าเขาต้องการ std :: shared_ptr รอบ ๆ หลังจากเรียกใช้ฟังก์ชันหรือไม่และขึ้นอยู่กับว่าจะย้ายหรือไม่ สิ่งนี้จะไม่สามารถทำได้ถ้าคุณผ่านเครื่องหมาย & & ดังนั้นจึงควรใช้ค่านี้

แน่นอนถ้าผู้เรียกทั้งสองต้องการ shared_ptr ของเขารอบนานกว่า (ดังนั้นไม่สามารถ std :: ย้าย) และคุณไม่ต้องการสร้างสำเนาธรรมดาในฟังก์ชั่น (พูดว่าคุณต้องการตัวชี้อ่อนแอหรือบางครั้งคุณต้องการเท่านั้น เพื่อคัดลอกมันขึ้นอยู่กับเงื่อนไขบางอย่าง) จากนั้นกลุ่ม & อาจจะยังดีกว่า

ตัวอย่างเช่นคุณควรทำ

void enqueue(std::shared<T> t) m_internal_queue.enqueue(std::move(t));

เกิน

void enqueue(std::shared<T> const& t) m_internal_queue.enqueue(t);

เพราะในกรณีนี้คุณมักจะสร้างสำเนาภายใน


1

ไม่ทราบว่าเสียเวลาในการดำเนินการคัดลอก shared_copy ซึ่งมีการเพิ่มขึ้นหรือลดลงของอะตอมฉันได้รับปัญหาการใช้งาน CPU สูงกว่ามาก ฉันไม่เคยคาดหวังว่าการเพิ่มขึ้นของอะตอมและการลดลงอาจมีค่าใช้จ่ายมาก

ตามผลการทดสอบของฉันการเพิ่มขึ้นและลดลงของอะตอม int32 ใช้เวลา 2 หรือ 40 ครั้งกว่าการเพิ่มและลดที่ไม่ใช่อะตอม ฉันใช้ 3GHz Core i7 กับ Windows 8.1 ผลลัพธ์ในอดีตจะเกิดขึ้นเมื่อไม่มีการช่วงชิงความเป็นไปได้ที่จะเกิดความขัดแย้งสูง ฉันจำไว้ว่าการทำงานของอะตอมนั้นเป็นการล็อคด้วยฮาร์ดแวร์ล่าสุด ล็อคคือล็อค ไม่ดีต่อประสิทธิภาพเมื่อเกิดการโต้แย้ง

พบสิ่งนี้ฉันมักจะใช้ byref (const shared_ptr &) กว่า byval (shared_ptr)


1

มีโพสต์บล็อกล่าสุดคือ: https://medium.com/@vgasparyan1995/pass-by-value-vs-pass-by-reference-to-const-c-f8944171e3ce

ดังนั้นคำตอบคือ: อย่าผ่านconst shared_ptr<T>&เลย
เพียงส่งคลาสพื้นฐานแทน

โดยทั่วไปประเภทพารามิเตอร์ที่เหมาะสมเท่านั้นคือ:

  • shared_ptr<T> - แก้ไขและเป็นเจ้าของ
  • shared_ptr<const T> - อย่าแก้ไขถือกรรมสิทธิ์
  • T& - แก้ไขไม่มีความเป็นเจ้าของ
  • const T& - อย่าแก้ไขไม่มีความเป็นเจ้าของ
  • T - อย่าแก้ไข, ไม่มีกรรมสิทธิ์, ถูกคัดลอก

ดังที่ @accel ชี้ให้เห็นในhttps://stackoverflow.com/a/26197326/1930508คำแนะนำจาก Herb Sutter คือ:

ใช้ const shared_ptr & เป็นพารามิเตอร์เฉพาะเมื่อคุณไม่แน่ใจว่าคุณจะคัดลอกและแบ่งปันความเป็นเจ้าของหรือไม่

แต่คุณไม่แน่ใจว่ามีกี่กรณี ดังนั้นนี่เป็นสถานการณ์ที่หายาก


0

เป็นปัญหาที่ทราบกันแล้วว่าการส่ง shared_ptr ด้วยมูลค่ามีค่าใช้จ่ายและควรหลีกเลี่ยงหากเป็นไปได้

ค่าใช้จ่ายในการผ่านโดย shared_ptr

เวลาส่วนใหญ่ที่ส่งผ่าน shared_ptr โดยการอ้างอิงและดียิ่งขึ้นโดยการอ้างอิง const จะทำ

แนวทางหลักของ cpp มีกฎเฉพาะสำหรับการส่ง shared_ptr

R.34: ใช้พารามิเตอร์ shared_ptr เพื่อแสดงว่าฟังก์ชั่นเป็นเจ้าของชิ้นส่วน

void share(shared_ptr<widget>);            // share -- "will" retain refcount

ตัวอย่างของการส่งผ่าน shared_ptr ด้วยมูลค่าเป็นสิ่งที่จำเป็นจริง ๆ เมื่อผู้เรียกส่งผ่านวัตถุที่ใช้ร่วมกันไปยังผู้เรียกแบบอะซิงโครนัส - นั่นคือผู้โทรออกจากขอบเขตก่อนที่ผู้ดำเนินการจะเสร็จสิ้น ผู้รับจะต้อง "ยืด" อายุการใช้งานของวัตถุที่ใช้ร่วมกันโดยการ share_ptr ตามค่า ในกรณีนี้การส่งการอ้างอิงไปยัง shared_ptr จะไม่ทำ

เช่นเดียวกับการส่งวัตถุที่แชร์ไปยังเธรดงาน


-4

shared_ptr ไม่ใหญ่พอหรือตัวสร้าง \ destructor ทำงานได้ไม่เพียงพอที่จะมีค่าใช้จ่ายเพียงพอจากการคัดลอกเพื่อดูแลเกี่ยวกับ pass โดยการอ้างอิง vs pass โดยการคัดลอกประสิทธิภาพ


15
คุณวัดมันได้หรือไม่
curiousguy

2
@stonemetal: จะมีคำแนะนำเกี่ยวกับปรมาณูระหว่างการสร้าง shared_ptr ใหม่ได้อย่างไร
Quarra

มันเป็นประเภทที่ไม่ใช่ POD ดังนั้นใน ABIs ส่วนใหญ่แม้จะผ่านมัน "โดยค่า" จะผ่านตัวชี้ ไม่ใช่การคัดลอกไบต์ที่เป็นปัญหาจริงๆ ดังที่คุณเห็นในเอาต์พุต asm การส่ง a shared_ptr<int>ตามค่าจะมีคำสั่งมากกว่า 100 x86 คำสั่ง (รวมถึงlockคำแนะนำ ed ที่มีราคาแพงไปยังการคำนวณแบบนับหน่วย / ธันวาคม) การส่งผ่านค่าอ้างอิงคงที่จะเหมือนกับการส่งตัวชี้ไปยังสิ่งใด ๆ (และในตัวอย่างนี้บนตัวรวบรวมคอมไพเลอร์ Godbolt การปรับให้เหมาะสมแบบหางเรียกจะเปลี่ยนสิ่งนี้ให้กลายเป็น jmp อย่างง่ายแทนการโทร: godbolt.org/g/TazMBU )
Peter Cordes

TL: DR: นี่คือ C ++ ที่ตัวสร้างสำเนาสามารถทำงานได้มากกว่าการคัดลอกไบต์ คำตอบนี้เป็นขยะทั้งหมด
Peter Cordes

2
stackoverflow.com/questions/3628081/shared-ptr-horrible-speedเป็นตัวอย่างพอยน์เตอร์ที่ใช้ร่วมกันที่ส่งโดย value vs pass โดยการอ้างอิงเขาเห็นความแตกต่างของเวลาทำงานประมาณ 33% หากคุณกำลังทำงานกับโค้ดที่สำคัญเกี่ยวกับประสิทธิภาพตัวชี้เปล่าจะทำให้คุณเพิ่มประสิทธิภาพได้มากขึ้น ดังนั้นโปรดผ่าน const อ้างอิงถ้าคุณจำได้ แต่มันไม่ใช่เรื่องใหญ่ถ้าคุณทำไม่ได้ มันสำคัญมากที่จะไม่ใช้ shared_ptr หากคุณไม่ต้องการ
หิน 11
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.