ความแตกต่างใน make_shared และ shared_ptr ปกติใน C ++


276
std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));

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

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


4
มันไม่มีประสิทธิภาพมากกว่านี้ เหตุผลในการใช้เพื่อความปลอดภัยยกเว้น
Yuushi

บางบทความบอกว่ามันหลีกเลี่ยงค่าใช้จ่ายในการก่อสร้างคุณช่วยอธิบายเพิ่มเติมเกี่ยวกับเรื่องนี้ได้ไหม?
Anup Buchke

16
@Yuushi: ความปลอดภัยยกเว้นเป็นเหตุผลที่ดีที่จะใช้ แต่ก็มีประสิทธิภาพมากขึ้น
Mike Seymour

3
32:15 เป็นที่ที่เขาเริ่มต้นในวิดีโอที่ฉันเชื่อมโยงกับด้านบนถ้ามันช่วยได้
chris

4
ข้อได้เปรียบสไตล์โค้ดเล็กน้อย: การใช้make_sharedคุณสามารถเขียนauto p1(std::make_shared<A>())และ p1 จะมีประเภทที่ถูกต้อง
Ivan Vergiliev

คำตอบ:


333

ความแตกต่างคือstd::make_sharedทำการจัดสรรฮีปหนึ่งครั้งขณะที่การเรียกคอนstd::shared_ptrสตรัคเตอร์ทำสองค่า

การจัดสรรฮีปเกิดขึ้นที่ไหน?

std::shared_ptr จัดการสองหน่วยงาน:

  • บล็อกควบคุม (เก็บข้อมูลเมตาเช่นการนับการอ้างอิงการลบประเภทการลบเป็นต้น)
  • วัตถุที่กำลังจัดการ

std::make_sharedดำเนินการบัญชีการจัดสรรฮีปเดียวสำหรับพื้นที่ที่จำเป็นสำหรับทั้งบล็อกควบคุมและข้อมูล ในกรณีอื่นnew Obj("foo")เรียกใช้การจัดสรรฮีปสำหรับข้อมูลที่มีการจัดการและตัวstd::shared_ptrสร้างจะดำเนินการอีกอันสำหรับบล็อกควบคุม

สำหรับข้อมูลเพิ่มเติมโปรดดูที่บันทึกการดำเนินงานที่cppreference

Update I: Exception-Safety

หมายเหตุ (2019/08/30) : นี่ไม่ใช่ปัญหาตั้งแต่ C ++ 17 เนื่องจากการเปลี่ยนแปลงในลำดับการประเมินผลของฟังก์ชันอาร์กิวเมนต์ โดยเฉพาะอย่างยิ่งอาร์กิวเมนต์แต่ละตัวในฟังก์ชั่นจะต้องดำเนินการอย่างเต็มที่ก่อนการประเมินผลของข้อโต้แย้งอื่น ๆ

เนื่องจาก OP ดูเหมือนจะสงสัยเกี่ยวกับข้อยกเว้นด้านความปลอดภัยของสิ่งต่าง ๆ ฉันจึงอัปเดตคำตอบของฉัน

ลองพิจารณาตัวอย่างนี้

void F(const std::shared_ptr<Lhs> &lhs, const std::shared_ptr<Rhs> &rhs) { /* ... */ }

F(std::shared_ptr<Lhs>(new Lhs("foo")),
  std::shared_ptr<Rhs>(new Rhs("bar")));

เนื่องจาก C ++ อนุญาตให้มีคำสั่งย่อยของการประเมินผลของคำสั่งย่อยโดยพลคำสั่งหนึ่งที่เป็นไปได้คือ:

  1. new Lhs("foo"))
  2. new Rhs("bar"))
  3. std::shared_ptr<Lhs>
  4. std::shared_ptr<Rhs>

ทีนี้สมมติว่าเราได้รับข้อผิดพลาดเกิดขึ้นในขั้นตอนที่ 2 (เช่นออกจากข้อยกเว้นหน่วยความจำตัวRhsสร้างโยนข้อยกเว้นบางอย่าง) เราสูญเสียหน่วยความจำที่จัดสรรในขั้นตอนที่ 1 เนื่องจากไม่มีสิ่งใดจะมีโอกาสล้างมัน แก่นของปัญหาตรงนี้คือตัวชี้แบบดิบไม่ได้ถูกส่งผ่านไปยังตัวstd::shared_ptrสร้างทันที

วิธีหนึ่งในการแก้ไขปัญหานี้คือการทำในบรรทัดที่แยกต่างหาก

auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);

วิธีที่ดีที่สุดในการแก้ไขปัญหานี้คือการใช้std::make_sharedแทน

F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

อัปเดต II: ข้อเสียของ std::make_shared

เธซเธฑเคซี่ย์เมนต์ 's:

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

เหตุใดอินสแตนซ์ของweak_ptrทำให้บล็อกควบคุมยังมีชีวิตอยู่

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

กลับไปยัง std::make_shared

เนื่องจากstd::make_sharedทำการจัดสรรฮีปเดียวสำหรับทั้งบล็อกควบคุมและวัตถุที่ได้รับการจัดการจึงไม่มีวิธีใดที่จะเพิ่มหน่วยความจำสำหรับบล็อกควบคุมและวัตถุที่ได้รับการจัดการอย่างอิสระ เราจะต้องรอจนกว่าเราสามารถฟรีทั้งบล็อกควบคุมและการจัดการวัตถุซึ่งจะเกิดขึ้นจนกว่าจะไม่มีshared_ptrหรือweak_ptrs ยังมีชีวิตอยู่

สมมติว่าเราทำการจัดสรรฮีปสองชุดสำหรับบล็อกควบคุมและอ็อบเจ็กต์ที่ถูกจัดการผ่านnewและตัวshared_ptrสร้าง จากนั้นเราเพิ่มหน่วยความจำสำหรับวัตถุที่มีการจัดการ (อาจก่อนหน้านี้) เมื่อไม่มีshared_ptrชีวิตและเพิ่มหน่วยความจำสำหรับบล็อกควบคุม (อาจจะภายหลัง) เมื่อไม่มีweak_ptrชีวิต


53
เป็นความคิดที่ดีที่จะกล่าวถึงข้อเสียเล็ก ๆ น้อย ๆmake_sharedเช่น: เนื่องจากมีการจัดสรรเพียงครั้งเดียวหน่วยความจำของปวงจึงไม่สามารถจัดสรรคืนได้จนกว่าบล็อกควบคุมจะไม่ใช้งานอีกต่อไป A weak_ptrสามารถทำให้บล็อกควบคุมยังมีชีวิตอยู่ได้ตลอดไป
Casey

14
อีกหนึ่งโวหารที่สำคัญกว่าคือถ้าคุณใช้make_sharedและmake_uniqueต่อเนื่องคุณจะไม่ได้เป็นเจ้าของพอยน์เตอร์พอยน์แบบใดและสามารถรักษาทุกครั้งที่เกิดnewกลิ่น
Philipp

6
หากมีเพียงหนึ่งเดียวshared_ptrและไม่มีweak_ptrs การเรียกreset()ใช้shared_ptrอินสแตนซ์จะลบบล็อกควบคุม แต่สิ่งนี้ไม่ว่าจะmake_sharedถูกใช้หรือไม่ก็ตาม การใช้make_sharedที่ทำให้แตกต่างเพราะมันสามารถยืดอายุการใช้เวลาของหน่วยความจำที่จัดสรรไว้สำหรับการจัดการวัตถุ เมื่อshared_ptrนับฮิต 0, destructor สำหรับวัตถุที่มีการจัดการที่ได้รับการเรียกว่าไม่คำนึงถึงmake_sharedแต่พ้นหน่วยความจำที่สามารถทำได้ถ้าmake_sharedถูกไม่ได้ใช้ หวังว่านี่จะทำให้ชัดเจนยิ่งขึ้น
mpark

4
นอกจากนี้ยังเป็นการคุ้มค่าที่จะกล่าวถึงว่า make_shared สามารถใช้ประโยชน์จากการเพิ่มประสิทธิภาพ "เรารู้ว่าคุณอยู่ที่ไหน" ที่อนุญาตให้บล็อกควบคุมเป็นตัวชี้ที่เล็กลง (สำหรับรายละเอียดโปรดดูการนำเสนอ GN2012 ของ Stephan T. Lavavej ในเวลาประมาณ 12 นาที) make_shared จึงไม่เพียง แต่หลีกเลี่ยงการจัดสรรเท่านั้น แต่ยังจัดสรรหน่วยความจำรวมให้น้อยลง
KnowItAllWannabe

1
@HannaKhalil: นี่อาจเป็นขอบเขตของสิ่งที่คุณกำลังมองหา ... ? melpon.org/wandbox/permlink/b5EpsiSxDeEz8lGH
mpark

26

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

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


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

22

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

class A
{
public:

    A(): val(0){}

    std::shared_ptr<A> createNext(){ return std::make_shared<A>(val+1); }
    // Invalid because make_shared needs to call A(int) **internally**

    std::shared_ptr<A> createNext(){ return std::shared_ptr<A>(new A(val+1)); }
    // Works fine because A(int) is called explicitly

private:

    int val;

    A(int v): val(v){}
};

ฉันวิ่งเข้าไปในปัญหาตรงนี้และตัดสินใจที่จะใช้อย่างอื่นผมจะได้ใช้new make_sharedนี่คือคำถามที่เกี่ยวข้องเกี่ยวกับที่: stackoverflow.com/questions/8147027/...
jigglypuff

6

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


2
สถานการณ์ที่สองที่ make_shared นั้นไม่เหมาะสมคือเมื่อคุณต้องการระบุ deleter ที่กำหนดเอง
KnowItAllWannabe

5

ฉันเห็นปัญหาหนึ่งเกี่ยวกับ std :: make_shared มันไม่สนับสนุนตัวสร้างส่วนตัว / ที่มีการป้องกัน


3

Shared_ptr: ทำการจัดสรรฮีปสองชุด

  1. บล็อกควบคุม (จำนวนการอ้างอิง)
  2. วัตถุถูกจัดการ

Make_shared: ทำการจัดสรรฮีปเดียวเท่านั้น

  1. ข้อมูลควบคุมบล็อกและวัตถุ

0

เกี่ยวกับประสิทธิภาพและเวลาที่ใช้ในการจัดสรรฉันทำแบบทดสอบง่ายๆด้านล่างนี้ฉันสร้างหลาย ๆ กรณีผ่านสองวิธีนี้ (ทีละครั้ง):

for (int k = 0 ; k < 30000000; ++k)
{
    // took more time than using new
    std::shared_ptr<int> foo = std::make_shared<int> (10);

    // was faster than using make_shared
    std::shared_ptr<int> foo2 = std::shared_ptr<int>(new int(10));
}

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


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

0

ฉันคิดว่าส่วนความปลอดภัยยกเว้นของคำตอบ mr mpark ยังคงเป็นข้อกังวลที่ถูกต้อง เมื่อสร้าง shared_ptr ดังนี้: shared_ptr <T> (new T) T ใหม่อาจสำเร็จในขณะที่การจัดสรรบล็อกควบคุมของ shared_ptr อาจล้มเหลว ในสถานการณ์นี้ T ที่ถูกจัดสรรใหม่จะรั่วไหลเนื่องจาก shared_ptr ไม่มีทางรู้ว่ามันถูกสร้างขึ้นในสถานที่และปลอดภัยที่จะลบ หรือฉันหายไปบางอย่าง ฉันไม่คิดว่ากฎที่เข้มงวดเกี่ยวกับการประเมินค่าพารามิเตอร์ฟังก์ชันช่วยในทางใด ๆ ที่นี่ ...

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