วิธีส่งคืนตัวชี้อัจฉริยะ (shared_ptr) โดยอ้างอิงหรือตามค่า


101

สมมติว่าฉันมีคลาสที่มีเมธอดที่ส่งกลับshared_ptr.

ประโยชน์ที่เป็นไปได้และข้อเสียของการส่งคืนโดยอ้างอิงหรือตามมูลค่าคืออะไร?

เบาะแสที่เป็นไปได้สองประการ:

  • การทำลายวัตถุในช่วงต้น ถ้าฉันส่งคืนการshared_ptrอ้างอิงโดย (const) ตัวนับการอ้างอิงจะไม่เพิ่มขึ้นดังนั้นฉันจึงมีความเสี่ยงที่จะถูกลบวัตถุเมื่อวัตถุนั้นอยู่นอกขอบเขตในบริบทอื่น (เช่นเธรดอื่น) ถูกต้องหรือไม่ จะเกิดอะไรขึ้นถ้าสภาพแวดล้อมเป็นเธรดเดียวสถานการณ์นี้จะเกิดขึ้นได้หรือไม่
  • ค่าใช้จ่าย Pass-by-value นั้นไม่ฟรีอย่างแน่นอน คุ้มค่าที่จะหลีกเลี่ยงทุกครั้งที่ทำได้หรือไม่?

ขอบคุณทุกๆคน.

คำตอบ:


118

ส่งคืนตัวชี้อัจฉริยะตามค่า

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

ปัจจุบันความกังวลเรื่องค่าใช้จ่ายเกิดขึ้นเนื่องจากการเพิ่มประสิทธิภาพการคืนค่า (RVO) ดังนั้นคุณจะไม่เกิดลำดับการเพิ่มขึ้น - ลดลงหรืออะไรทำนองนั้นในคอมไพเลอร์สมัยใหม่ ดังนั้นวิธีที่ดีที่สุดในการคืนค่า a shared_ptrคือเพียงแค่ส่งคืนค่า:

shared_ptr<T> Foo()
{
    return shared_ptr<T>(/* acquire something */);
};

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

หากคุณยังไม่มั่นใจ Dave Abrahams มีบทความที่โต้แย้งการคืนค่าตามมูลค่า ฉันสร้างข้อมูลโค้ดซ้ำที่นี่ ฉันขอแนะนำให้คุณอ่านบทความทั้งหมด:

ซื่อสัตย์: รหัสต่อไปนี้ทำให้คุณรู้สึกอย่างไร?

std::vector<std::string> get_names();
...
std::vector<std::string> const names = get_names();

ตรงไปตรงมาแม้ว่าฉันควรจะรู้ดีกว่า แต่มันก็ทำให้ฉันกังวล โดยหลักการแล้วเมื่อget_names() ส่งคืนเราต้องคัดลอก a vectorของstrings จากนั้นเราต้องคัดลอกอีกครั้งเมื่อเริ่มต้น namesและเราต้องทำลายสำเนาแรก หากมี N strings อยู่ในเวกเตอร์แต่ละสำเนาอาจต้องการการจัดสรรหน่วยความจำ N + 1 มากถึง N + 1 และการเข้าถึงข้อมูลที่ไม่เป็นมิตรกับแคชทั้งหมด> เนื่องจากมีการคัดลอกเนื้อหาสตริง

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

get_names(std::vector<std::string>& out_param );
...
std::vector<std::string> names;
get_names( names );

น่าเสียดายที่แนวทางนี้ยังห่างไกลจากอุดมคติ

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

แต่จำเป็นจริงๆหรือที่จะทำให้โค้ดของเรายุ่งเหยิงเพื่อให้ได้ประสิทธิภาพ โชคดีที่คำตอบกลายเป็นไม่ (และโดยเฉพาะอย่างยิ่งถ้าคุณใช้ C ++ 0x)


ฉันไม่รู้ว่าฉันจะบอกว่า RVO ทำให้คำถามสงสัยตั้งแต่กลับมาโดยการอ้างอิงทำให้ RVO เป็นไปไม่ได้
Edward Strange

@CrazyEddie: จริงนั่นคือเหตุผลหนึ่งที่ฉันแนะนำให้ OP ส่งคืนตามค่า
ในซิลิโค

กฎ RVO ที่ได้รับอนุญาตตามมาตรฐานกำหนดกฎเกี่ยวกับการซิงโครไนซ์ / ความสัมพันธ์ที่เกิดขึ้นก่อนที่จะรับประกันโดยมาตรฐานหรือไม่
edA-qa mort-ora-y

1
@ edA-qa mort-ora-y: อนุญาตให้ใช้ RVO อย่างชัดเจนแม้ว่าจะมีผลข้างเคียงก็ตาม ตัวอย่างเช่นหากคุณมีcout << "Hello World!";คำสั่งเป็นค่าเริ่มต้นและคัดลอกตัวสร้างคุณจะไม่เห็นสองHello World!วินาทีเมื่อ RVO มีผลบังคับใช้ อย่างไรก็ตามสิ่งนี้ไม่ควรเป็นปัญหาสำหรับตัวชี้อัจฉริยะที่ออกแบบมาอย่างเหมาะสมแม้กระทั่งการซิงโครไนซ์แบบ WRT
ในซิลิโค

23

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

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

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

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