C ++ - ส่งต่อการอ้างอิงไปยัง std :: shared_ptr หรือ boost :: shared_ptr


115

ถ้าฉันมีฟังก์ชันที่ต้องทำงานร่วมกับ a shared_ptrมันจะไม่มีประสิทธิภาพมากกว่าที่จะส่งต่อการอ้างอิงไปยังมัน (เพื่อหลีกเลี่ยงการคัดลอกshared_ptrวัตถุ) ผลข้างเคียงที่ไม่ดีที่เป็นไปได้คืออะไร? ฉันนึกภาพสองกรณีที่เป็นไปได้:

1) ภายในฟังก์ชันมีสำเนาของอาร์กิวเมนต์เช่นใน

ClassA::take_copy_of_sp(boost::shared_ptr<foo> &sp)  
{  
     ...  
     m_sp_member=sp; //This will copy the object, incrementing refcount  
     ...  
}  

2) ภายในฟังก์ชันจะใช้อาร์กิวเมนต์เท่านั้นเช่นใน

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{    
    ...  
    sp->do_something();  
    ...  
}  

ฉันไม่เห็นเหตุผลที่ดีในทั้งสองกรณีที่จะส่งผ่านboost::shared_ptr<foo>ค่าโดยแทนที่จะเป็นการอ้างอิง การส่งผ่านตามค่าจะเพิ่มจำนวนอ้างอิง "ชั่วคราว" เนื่องจากการคัดลอกและลดลงเมื่อออกจากขอบเขตฟังก์ชัน ฉันมองข้ามบางสิ่งไปหรือเปล่า?

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


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

คำตอบ:


113

ประเด็นของshared_ptrอินสแตนซ์ที่แตกต่างกันคือการรับประกัน (เท่าที่จะทำได้) ว่าตราบใดที่สิ่งนี้shared_ptrอยู่ในขอบเขตวัตถุที่ชี้ไปจะยังคงมีอยู่เนื่องจากจำนวนการอ้างอิงจะมีค่าอย่างน้อย 1

Class::only_work_with_sp(boost::shared_ptr<foo> sp)
{
    // sp points to an object that cannot be destroyed during this function
}

ดังนั้นโดยใช้การอ้างอิงถึงshared_ptrคุณจึงปิดใช้งานการรับประกันนั้น ดังนั้นในกรณีที่สองของคุณ:

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{    
    ...  
    sp->do_something();  
    ...  
}

คุณจะรู้ได้อย่างไรว่าsp->do_something()จะไม่ระเบิดเนื่องจากตัวชี้โมฆะ?

ทุกอย่างขึ้นอยู่กับสิ่งที่อยู่ในส่วน "... " ของโค้ด จะเกิดอะไรขึ้นถ้าคุณเรียกบางสิ่งในช่วงแรก '... ' ที่มีผลข้างเคียง (บางแห่งในส่วนอื่นของรหัส) ในการล้าง a shared_ptrไปยังวัตถุเดียวกันนั้น? และจะเกิดอะไรขึ้นถ้ามันเป็นสิ่งเดียวที่เหลืออยู่ของshared_ptrวัตถุนั้น? ลาก่อนวัตถุที่คุณกำลังจะลองใช้

ดังนั้นจึงมีสองวิธีในการตอบคำถามนี้:

  1. ตรวจสอบแหล่งที่มาของโปรแกรมทั้งหมดของคุณอย่างรอบคอบจนกว่าคุณจะแน่ใจว่าวัตถุนั้นจะไม่ตายในระหว่างร่างกายของฟังก์ชัน

  2. เปลี่ยนพารามิเตอร์กลับเป็นวัตถุที่แตกต่างกันแทนที่จะเป็นการอ้างอิง

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

อัปเดตสำหรับผู้แสดงความคิดเห็น JQ

นี่คือตัวอย่างที่สร้างขึ้น มันง่ายอย่างจงใจดังนั้นข้อผิดพลาดจะชัดเจน ในตัวอย่างจริงข้อผิดพลาดไม่ชัดเจนนักเนื่องจากซ่อนอยู่ในชั้นของรายละเอียดที่แท้จริง

เรามีฟังก์ชันที่จะส่งข้อความไปที่ไหนสักแห่ง อาจเป็นข้อความขนาดใหญ่ดังนั้นแทนที่จะใช้ข้อความstd::stringที่อาจถูกคัดลอกเนื่องจากถูกส่งไปยังที่ต่างๆเราใช้ a shared_ptrto a string:

void send_message(std::shared_ptr<std::string> msg)
{
    std::cout << (*msg.get()) << std::endl;
}

(เราเพียงแค่ "ส่ง" ไปยังคอนโซลสำหรับตัวอย่างนี้)

ตอนนี้เราต้องการเพิ่มสิ่งอำนวยความสะดวกในการจำข้อความก่อนหน้านี้ เราต้องการลักษณะการทำงานต่อไปนี้: ต้องมีตัวแปรที่มีข้อความที่ส่งล่าสุด แต่ในขณะที่กำลังส่งข้อความจะต้องไม่มีข้อความก่อนหน้านี้ (ควรรีเซ็ตตัวแปรก่อนส่ง) ดังนั้นเราจึงประกาศตัวแปรใหม่:

std::shared_ptr<std::string> previous_message;

จากนั้นเราแก้ไขการทำงานของเราตามกฎที่เราระบุ:

void send_message(std::shared_ptr<std::string> msg)
{
    previous_message = 0;
    std::cout << *msg << std::endl;
    previous_message = msg;
}

ดังนั้นก่อนที่เราจะเริ่มส่งเราจะทิ้งข้อความก่อนหน้าปัจจุบันและหลังจากการส่งเสร็จสมบูรณ์เราสามารถจัดเก็บข้อความก่อนหน้าใหม่ได้ ทั้งหมดดี. นี่คือรหัสทดสอบบางส่วน:

send_message(std::shared_ptr<std::string>(new std::string("Hi")));
send_message(previous_message);

และตามที่คาดไว้สิ่งนี้จะพิมพ์Hi!สองครั้ง

ตอนนี้นายดูแลรักษาผู้ซึ่งดูรหัสและคิดว่า: เฮ้พารามิเตอร์ที่send_messageจะเป็นshared_ptr:

void send_message(std::shared_ptr<std::string> msg)

เห็นได้ชัดว่าสามารถเปลี่ยนเป็น:

void send_message(const std::shared_ptr<std::string> &msg)

ลองนึกถึงการเพิ่มประสิทธิภาพนี้! (ไม่เป็นไรว่าปกติแล้วเรากำลังจะส่งข้อความขนาดใหญ่ไปยังบางช่องดังนั้นการเพิ่มประสิทธิภาพจะน้อยจนไม่สามารถวัดได้)

แต่ปัญหาที่แท้จริงคือตอนนี้โค้ดทดสอบจะแสดงพฤติกรรมที่ไม่ได้กำหนดไว้ (ในการสร้างการดีบัก Visual C ++ 2010 มันขัดข้อง)

Mr Maintainer รู้สึกประหลาดใจกับสิ่งนี้ แต่เพิ่มการตรวจสอบการป้องกันเพื่อsend_messageพยายามหยุดปัญหาที่เกิดขึ้น:

void send_message(const std::shared_ptr<std::string> &msg)
{
    if (msg == 0)
        return;

แต่แน่นอนว่ามันยังคงดำเนินต่อไปและหยุดทำงานเพราะmsgจะไม่เป็นโมฆะเมื่อsend_messageถูกเรียก

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

ทางออกที่ง่ายที่คุณต้องการฟังก์ชั่นเพื่อให้สามารถพึ่งพาshared_ptrดำเนินการต่อไปจะไม่ใช่ null ตลอดสำหรับฟังก์ชั่นในการจัดสรรที่แท้จริงของตัวเองมากกว่าการพึ่งพาการอ้างอิงไปยังมีอยู่shared_ptrshared_ptr

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

โปรดทราบว่าข้อผิดพลาดที่คล้ายกันจะเกิดขึ้นหากเราใช้std::stringตลอดแทนที่จะใช้std::shared_ptr<std::string>และแทนที่จะเป็น

previous_message = 0;

เพื่อล้างข้อความเราพูดว่า:

previous_message.clear();

จากนั้นอาการจะเป็นการส่งข้อความเปล่าโดยไม่ได้ตั้งใจแทนที่จะเป็นพฤติกรรมที่ไม่ได้กำหนด ค่าใช้จ่ายของสำเนาพิเศษของสตริงขนาดใหญ่มากอาจมีความสำคัญมากกว่าต้นทุนการคัดลอก a shared_ptrมากดังนั้นการแลกเปลี่ยนอาจแตกต่างกัน


16
shared_ptr ที่ส่งผ่านอยู่ในขอบเขตอยู่แล้วที่ไซต์การโทร คุณอาจสามารถสร้างสถานการณ์ที่ซับซ้อนซึ่งโค้ดในคำถามนี้จะระเบิดเนื่องจากตัวชี้ห้อย แต่ฉันคิดว่าคุณมีปัญหาใหญ่กว่าพารามิเตอร์อ้างอิง!
Magnus Hoff

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

7
มันไม่ใช่มุมมองของฉันจริงๆ! หากคุณคิดว่าสิ่งที่ฉันพูดเป็นสิ่งที่เฉพาะเจาะจงเกี่ยวกับรหัสของฉันคุณอาจไม่เข้าใจฉัน ฉันกำลังพูดถึงผลกระทบที่หลีกเลี่ยงไม่ได้ของเหตุผลที่ shared_ptr มีอยู่ตั้งแต่แรก: อายุการใช้งานของวัตถุจำนวนมากไม่ได้เกี่ยวข้องกับการเรียกใช้ฟังก์ชันเท่านั้น
Daniel Earwicker

8
@DanielEarwicker เห็นด้วยกับทุกประเด็นของคุณและรู้สึกประหลาดใจกับระดับการต่อต้าน สิ่งที่ทำให้ข้อกังวลของคุณมีความเกี่ยวข้องมากขึ้นคือเธรดเมื่อสิ่งนี้เกี่ยวข้องกับการรับประกันเกี่ยวกับความถูกต้องของวัตถุมีความสำคัญมากขึ้น คำตอบที่ดี.
radman

3
เมื่อไม่นานมานี้ฉันได้ไล่ตามข้อบกพร่องที่ร้ายแรงมากซึ่งเกิดจากการอ้างอิงถึงตัวชี้ที่ใช้ร่วมกัน รหัสกำลังจัดการกับการเปลี่ยนแปลงสถานะของวัตถุและเมื่อสังเกตเห็นว่าสถานะของวัตถุเปลี่ยนไปรหัสนั้นจะถูกลบออกจากการรวบรวมวัตถุในสถานะก่อนหน้าและย้ายไปไว้ในคอลเล็กชันของวัตถุในสถานะใหม่ การดำเนินการลบทำลายตัวชี้ที่ใช้ร่วมกันล่าสุดไปยังวัตถุ ฟังก์ชันสมาชิกถูกเรียกโดยอ้างอิงถึงตัวชี้ที่แบ่งใช้ในคอลเลกชัน ความเจริญ Daniel Earwicker พูดถูก
David Schwartz

115

ฉันพบว่าตัวเองไม่เห็นด้วยกับคำตอบที่ได้รับการโหวตสูงสุดดังนั้นฉันจึงมองหาผู้ตรวจสอบผู้เชี่ยวชาญและพวกเขาอยู่ที่นี่ จากhttp://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything

Herb Sutter: "เมื่อคุณส่ง shared_ptrs สำเนาจะมีราคาแพง"

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

Herb Sutter: "ส่งผ่านโดยอ้างอิงถึง const เสมอและบางครั้งอาจเป็นเพราะคุณรู้ว่าสิ่งที่คุณเรียกอาจแก้ไขสิ่งที่คุณได้รับการอ้างอิงจากนั้นคุณอาจส่งผ่านค่า ... ถ้าคุณคัดลอกเป็นพารามิเตอร์โอ้ ความดีของฉันคุณแทบไม่จำเป็นต้องลดจำนวนการอ้างอิงนั้นเพราะมันยังคงมีชีวิตอยู่และคุณควรจะส่งต่อโดยการอ้างอิงดังนั้นโปรดทำเช่นนั้น "

อัปเดต: สมุนไพรได้ขยายความไว้ที่นี่: http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/แม้ว่าคุณธรรมของเรื่องราวคือคุณไม่ควรผ่านไป shared_ptrs เลย "เว้นแต่คุณต้องการใช้หรือจัดการกับตัวชี้อัจฉริยะเองเช่นเพื่อแบ่งปันหรือโอนความเป็นเจ้าของ"


8
พบดี! เป็นเรื่องดีที่ได้เห็นผู้เชี่ยวชาญที่สำคัญที่สุดสองคนในหัวข้อนี้เปิดเผยต่อสาธารณชนเกี่ยวกับภูมิปัญญาดั้งเดิมของ SO
Stephan Tolksdorf

3
"ไม่มีอะไรพิเศษเกี่ยวกับ shared_ptr เมื่อพูดถึงว่าคุณส่งผ่านค่าหรือส่งผ่านโดยอ้างอิง" - ฉันไม่เห็นด้วยกับสิ่งนี้จริงๆ เป็นพิเศษ โดยส่วนตัวแล้วฉันค่อนข้างจะเล่นอย่างปลอดภัยและได้รับผลกระทบเล็กน้อย หากมีพื้นที่เฉพาะของโค้ดฉันต้องปรับให้เหมาะสมแน่นอนฉันจะดูประโยชน์ด้านประสิทธิภาพของ shared_ptr pass โดย const ref
JasonZ

3
นอกจากนี้ยังน่าสนใจที่จะทราบว่าในขณะที่มีข้อตกลงเกี่ยวกับการใช้งานมากเกินไปshared_ptrแต่ก็ไม่มีข้อตกลงเกี่ยวกับปัญหา pass-by-value-vs. -reference
Nicol Bolas

2
สก็อตต์เมเยอร์ส: "ดังนั้นถ้าฉันสามารถหลีกเลี่ยงมันด้วยความหมายที่เหมาะสมในโปรแกรมของฉัน ... " กล่าวคือไม่ขัดแย้งกับคำตอบของฉันเลยซึ่งชี้ให้เห็นว่าการหาว่าการเปลี่ยนพารามิเตอร์const &จะส่งผลต่อความหมายนั้นง่ายมาก โปรแกรมง่ายๆ
Daniel Earwicker

7
Herb Sutter: "บางครั้งอาจเป็นเพราะคุณรู้ว่าสิ่งที่คุณเรียกว่าอาจแก้ไขสิ่งที่คุณได้รับการอ้างอิงจาก" อีกครั้งที่ได้รับการยกเว้นเล็กน้อยสำหรับคดีเล็กน้อยดังนั้นจึงไม่ขัดแย้งกับคำตอบของฉัน คำถามยังคงอยู่: คุณรู้ได้อย่างไรว่าปลอดภัยที่จะใช้ const ref พิสูจน์ได้ง่ายมากในโปรแกรมธรรมดาไม่ใช่เรื่องง่ายในโปรแกรมที่ซับซ้อน แต่เดี๋ยวก่อนนี่คือ C ++ ดังนั้นเราจึงชอบการเพิ่มประสิทธิภาพไมโครก่อนวัยอันควรมากกว่าข้อกังวลด้านวิศวกรรมอื่น ๆ เกือบทั้งหมดใช่มั้ย! :)
Daniel Earwicker

22

ฉันขอแนะนำให้ปฏิบัติตามนี้เว้นแต่คุณและโปรแกรมเมอร์คนอื่น ๆ ที่คุณทำงานด้วยจริงๆจะรู้ดีว่าคุณกำลังทำอะไรอยู่

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

ประการที่สองอย่าเพิ่มประสิทธิภาพจนกว่าคุณจะรู้ว่าคลาสนี้กำลังมีปัญหา โปรไฟล์ก่อนจากนั้นหากโปรแกรมของคุณต้องการการเพิ่มประสิทธิภาพจากการอ้างอิงจริงๆก็อาจจะ มิฉะนั้นอย่าใช้สิ่งเล็ก ๆ น้อย ๆ (เช่นคำสั่ง N พิเศษที่ใช้ในการส่งผ่านค่า) แทนที่จะกังวลเกี่ยวกับการออกแบบโครงสร้างข้อมูลอัลกอริทึมและการบำรุงรักษาในระยะยาว


แม้ว่าคำตอบของ litb จะถูกต้องในทางเทคนิค แต่อย่าดูถูก "ความขี้เกียจ" ของโปรแกรมเมอร์ (ฉันก็ขี้เกียจเหมือนกัน!) คำตอบของ littlenag ดีกว่าคือการอ้างอิงถึง shared_ptr จะเป็นสิ่งที่ไม่คาดคิดและอาจ (อาจ) เป็นการเพิ่มประสิทธิภาพที่ไม่จำเป็นซึ่งทำให้การบำรุงรักษาในอนาคตมีความท้าทายมากขึ้น
netjeff

18

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

void ClassA::take_copy_of_sp(boost::shared_ptr<foo> sp) {
    m_sp_member.swap(sp);
}

คุณควรคัดลอกเมื่อคุณส่งคืน (เช่นไม่ส่งคืนข้อมูลอ้างอิง) เนื่องจากชั้นเรียนของคุณไม่ทราบว่าลูกค้ากำลังทำอะไรกับมัน (มันสามารถจัดเก็บตัวชี้ไปที่มันได้แล้วบิ๊กแบงก็เกิดขึ้น) หากปรากฎในภายหลังว่าเป็นคอขวด (โปรไฟล์แรก!) คุณยังสามารถส่งคืนข้อมูลอ้างอิงได้


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


11

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

พยายามใช้const&แทนที่จะใช้เพียง&เพราะวัตถุชั่วคราวอาจไม่ถูกส่งผ่านโดยการอ้างอิงที่ไม่ใช่ const (แม้ว่าส่วนขยายภาษาใน MSVC จะอนุญาตให้คุณทำได้ก็ตาม)


3
ใช่ฉันใช้การอ้างอิง const เสมอฉันลืมใส่ในตัวอย่างของฉัน อย่างไรก็ตาม MSVC อนุญาตให้เชื่อมโยงการอ้างอิงที่ไม่ใช่ const เข้ากับ temporaries ไม่ใช่สำหรับจุดบกพร่อง แต่เนื่องจากโดยค่าเริ่มต้นจะมีคุณสมบัติ "C / C ++ -> Language -> Disable Language Extension" ที่ตั้งค่าเป็น "NO" เปิดใช้งานและจะไม่รวบรวมข้อมูล ...
abigagli

abigagli: อย่างจริงจัง? หวาน! ฉันจะบังคับใช้สิ่งนี้ในที่ทำงานสิ่งแรกในวันพรุ่งนี้)
Magnus Hoff

10

ในกรณีที่สองการทำเช่นนี้ง่ายกว่า:

Class::only_work_with_sp(foo &sp)
{    
    ...  
    sp.do_something();  
    ...  
}

คุณสามารถเรียกมันว่า

only_work_with_sp(*sp);

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

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

3

ฉันจะหลีกเลี่ยงการอ้างอิง "ธรรมดา" เว้นแต่ว่าฟังก์ชันอาจแก้ไขตัวชี้ได้อย่างชัดเจน

A const &อาจเป็นการเพิ่มประสิทธิภาพแบบไมโครที่สมเหตุสมผลเมื่อเรียกใช้ฟังก์ชันขนาดเล็กเช่นเพื่อเปิดใช้งานการเพิ่มประสิทธิภาพเพิ่มเติมเช่นการแทรกเงื่อนไขบางอย่างออกไป นอกจากนี้การเพิ่ม / ลด - เนื่องจากเป็นเธรดที่ปลอดภัย - เป็นจุดซิงโครไนซ์ ฉันไม่คาดหวังว่าสิ่งนี้จะสร้างความแตกต่างอย่างมากในสถานการณ์ส่วนใหญ่

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


3

ฉันจะสนับสนุนการส่งผ่านตัวชี้ที่ใช้ร่วมกันโดยการอ้างอิง const ซึ่งเป็นความหมายที่ว่าฟังก์ชันที่ส่งผ่านด้วยตัวชี้ไม่ได้เป็นเจ้าของตัวชี้ซึ่งเป็นสำนวนที่ชัดเจนสำหรับนักพัฒนา

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

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

ฉันเดาว่ามันไม่เกี่ยวกับการเพิ่มประสิทธิภาพก่อนเวลาอันควร - มันเกี่ยวกับการหลีกเลี่ยงการสิ้นเปลืองของวงจร CPU โดยไม่จำเป็นเมื่อคุณชัดเจนว่าคุณต้องการทำอะไรและสำนวนการเข้ารหัสได้รับการยอมรับอย่างแน่นหนาจากนักพัฒนาเพื่อนของคุณ

แค่ 2 เซ็นต์ของฉัน :-)


1
ดูความคิดเห็นด้านบนของ David Schwartz "... ฉันไล่ตามจุดบกพร่องที่ร้ายแรงมากซึ่งเกิดจากการส่งการอ้างอิงไปยังตัวชี้ที่ใช้ร่วมกันรหัสกำลังจัดการกับการเปลี่ยนแปลงสถานะของวัตถุและเมื่อสังเกตเห็นว่าสถานะของวัตถุเปลี่ยนไป มันลบออกจากคอลเลกชันของอ็อบเจ็กต์ในสถานะก่อนหน้าและย้ายไปยังคอลเลกชันของอ็อบเจ็กต์ในสถานะใหม่การดำเนินการลบทำลายพอยน์เตอร์ที่แบ่งใช้ล่าสุดไปยังอ็อบเจ็กต์ฟังก์ชันสมาชิกถูกเรียกโดยอ้างอิงไปยังพอยน์เตอร์ที่แบ่งใช้ ในคอลเลคชัน Boom ... "
Jason Harrison

3

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

เพื่อกลับไปที่ตัวอย่าง:

Class::only_work_with_sp( foo &sp ) //Again, no copy here  
{    
    ...  
    sp.do_something();  
    ...  
}

คุณรู้ได้อย่างไรว่าsp.do_something()จะไม่ระเบิดเนื่องจากตัวชี้ห้อย?

ความจริงก็คือ shared_ptr หรือไม่ const หรือไม่สิ่งนี้อาจเกิดขึ้นได้หากคุณมีข้อบกพร่องในการออกแบบเช่นการแบ่งปันความเป็นเจ้าของspระหว่างเธรดทั้งทางตรงและทางอ้อมพลาดการใช้วัตถุที่ทำdelete thisคุณมีความเป็นเจ้าของแบบวงกลมหรือข้อผิดพลาดในการเป็นเจ้าของอื่น ๆ


2

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

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

#include <boost/shared_ptr.hpp>

class Base { };
class Derived: public Base { };

// ONLY instances of Base can be passed by reference.  If you have a shared_ptr
// to a derived type, you have to cast it manually.  If you remove the reference
// and pass the shared_ptr by value, then the cast is implicit so you don't have
// to worry about it.
void test(boost::shared_ptr<Base>& b)
{
    return;
}

int main(void)
{
    boost::shared_ptr<Derived> d(new Derived);
    test(d);

    // If you want the above call to work with references, you will have to manually cast
    // pointers like this, EVERY time you call the function.  Since you are creating a new
    // shared pointer, you lose the benefit of passing by reference.
    boost::shared_ptr<Base> b = boost::dynamic_pointer_cast<Base>(d);
    test(b);

    return 0;
}

1

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

ผ่านการอ้างอิงไม่เป็นไร

การส่งผ่านการอ้างอิง const จะดีกว่าและโดยปกติแล้วสามารถใช้ได้เนื่องจากไม่ได้บังคับให้ const-ness บนวัตถุที่ชี้ไป

คุณไม่เสี่ยงต่อการสูญเสียตัวชี้เนื่องจากการใช้ข้อมูลอ้างอิง การอ้างอิงนั้นเป็นหลักฐานว่าคุณมีสำเนาของตัวชี้อัจฉริยะก่อนหน้านี้ในสแต็กและมีเธรดเดียวเท่านั้นที่เป็นเจ้าของ call stack ดังนั้นสำเนาที่มีอยู่แล้วจะไม่หายไป

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

คุณควรหลีกเลี่ยงการจัดเก็บตัวชี้อัจฉริยะของคุณไว้เป็นข้อมูลอ้างอิงเสมอ Class::take_copy_of_sp(&sp)ตัวอย่างของคุณแสดงการใช้งานที่ถูกต้องสำหรับสิ่งนั้น


1
"คุณไม่มีความเสี่ยงที่จะสูญเสียตัวชี้เนื่องจากการใช้ข้อมูลอ้างอิงการอ้างอิงนั้นเป็นหลักฐานว่าคุณมีสำเนาของตัวชี้อัจฉริยะก่อนหน้านี้ในสแตก" หรือสมาชิกข้อมูล ... ?
Daniel Earwicker

พิจารณาความมหัศจรรย์ของ boost :: thread และ boost :: ref: boost :: function <int> functionPointer = boost :: bind (doSomething, boost :: ref (sharedPtrInstance)); m_workerThread = เพิ่มใหม่ :: เธรด (functionPointer); ... ลบ sharedPtrInstance
Jason Harrison

1

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

void FooTakesReference( boost::shared_ptr< int > & ptr )
{
    ptr.reset(); // We reset, and so does sharedA, memory is deleted.
}

void FooTakesValue( boost::shared_ptr< int > ptr )
{
    ptr.reset(); // Our temporary is reset, however sharedB hasn't.
}

void main()
{
    boost::shared_ptr< int > sharedA( new int( 13 ) );
    boost::shared_ptr< int > sharedB( new int( 14 ) );

    FooTakesReference( sharedA );

    FooTakesValue( sharedB );
}

จากตัวอย่างด้านบนเราจะเห็นว่าการส่งsharedAโดยการอ้างอิงช่วยให้FooTakesReferenceรีเซ็ตตัวชี้เดิมซึ่งจะลดการใช้นับเป็น 0 ทำลายข้อมูล อย่างไรก็ตามFooTakesValueไม่สามารถรีเซ็ตตัวชี้เดิมได้รับประกันว่าข้อมูลของ sharedBยังคงใช้งานได้ เมื่อนักพัฒนารายอื่นเข้ามาอย่างหลีกเลี่ยงไม่ได้และพยายามที่จะเล่นหมูกับการดำรงอยู่ที่เปราะบางของ sharedAความโกลาหลก็เกิดขึ้น อย่างไรก็ตามนักพัฒนาsharedBผู้โชคดีกลับบ้านเร็วเพราะทุกอย่างอยู่ในโลกของเขา

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


1

แซนดี้เขียนว่า: "ดูเหมือนว่าข้อดีและข้อเสียทั้งหมดที่นี่สามารถสรุปได้กับทุกประเภทที่ส่งผ่านโดยการอ้างอิงไม่ใช่แค่ shared_ptr"

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

ฉันเขียนว่า "เกือบ" ในประโยคก่อนหน้านั้นเนื่องจากประสิทธิภาพอาจเป็นปัญหาและ 'อาจ' เป็นสิ่งที่สมเหตุสมผลในบางกรณี แต่ฉันก็จะหลีกเลี่ยงสถานการณ์นี้ด้วยตัวเองและมองหาโซลูชันการเพิ่มประสิทธิภาพอื่น ๆ ที่เป็นไปได้ทั้งหมดด้วยตัวเองเช่นดูอย่างจริงจัง ในการเพิ่มอีกระดับของทิศทางการประเมินแบบขี้เกียจ ฯลฯ ..

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


1

ฉันเคยทำงานในโครงการที่หลักการนี้แข็งแกร่งมากเกี่ยวกับการส่งสมาร์ทพอยน์เตอร์ตามมูลค่า เมื่อฉันถูกขอให้ทำการวิเคราะห์ประสิทธิภาพ - ฉันพบว่าสำหรับการเพิ่มและลดของตัวนับอ้างอิงของตัวชี้อัจฉริยะแอปพลิเคชันใช้เวลาระหว่าง 4-6% ของเวลาโปรเซสเซอร์ที่ใช้

หากคุณต้องการส่งผ่านตัวชี้อัจฉริยะตามค่าเพียงเพื่อหลีกเลี่ยงปัญหาในกรณีแปลก ๆ ตามที่อธิบายไว้จาก Daniel Earwicker โปรดตรวจสอบว่าคุณเข้าใจราคาที่คุณจ่ายไป

หากคุณตัดสินใจที่จะใช้การอ้างอิงเหตุผลหลักในการใช้การอ้างอิง const คือทำให้สามารถอัปคาสต์โดยปริยายเมื่อคุณต้องการส่งตัวชี้ที่ใช้ร่วมกันไปยังวัตถุจากคลาสที่สืบทอดคลาสที่คุณใช้ในอินเทอร์เฟซ


0

นอกเหนือจากสิ่งที่ litb กล่าวแล้วฉันอยากจะชี้ให้เห็นว่ามันอาจจะผ่านการอ้างอิง const ในตัวอย่างที่สองด้วยวิธีนี้คุณจะมั่นใจได้ว่าคุณไม่ได้แก้ไขโดยไม่ได้ตั้งใจ


0
struct A {
  shared_ptr<Message> msg;
  shared_ptr<Message> * ptr_msg;
}
  1. ผ่านค่า:

    void set(shared_ptr<Message> msg) {
      this->msg = msg; /// create a new shared_ptr, reference count will be added;
    } /// out of method, new created shared_ptr will be deleted, of course, reference count also be reduced;
  2. ผ่านการอ้างอิง:

    void set(shared_ptr<Message>& msg) {
     this->msg = msg; /// reference count will be added, because reference is just an alias.
     }
  3. ผ่านตัวชี้:

    void set(shared_ptr<Message>* msg) {
      this->ptr_msg = msg; /// reference count will not be added;
    }

0

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

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

ใช่ IMO ค่าเริ่มต้นควรเป็น " pass by const reference "

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