raw, weak_ptr, unique_ptr, shared_ptr ฯลฯ ... วิธีการเลือกอย่างชาญฉลาด?


33

มีพอยน์เตอร์จำนวนมากใน C ++ แต่จะซื่อสัตย์ใน 5 ปีหรือมากกว่านั้นในการเขียนโปรแกรม C ++ (โดยเฉพาะกับ Qt Framework) ฉันใช้พอยน์เตอร์พอยต์แบบเก่าเท่านั้น:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

ฉันรู้ว่ามีคำแนะนำ "ฉลาด" อื่น ๆ อีกมากมาย:

// shared pointer:
shared_ptr<SomeKindofObject> Object;

// unique pointer:
unique_ptr<SomeKindofObject> Object;

// weak pointer:
weak_ptr<SomeKindofObject> Object;

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

ตัวอย่างเช่นฉันมีส่วนหัวของชั้นนี้:

#ifndef LIBRARY
#define LIBRARY

class LIBRARY
{
public:
    // Permanent list that will be updated from time to time where
    // each items can be modified everywhere in the code:
    QList<ItemThatWillBeUsedEveryWhere*> listOfUselessThings; 
private:
    // Temporary reader that will read something to put in the list
    // and be quickly deleted:
    QSettings *_reader;
    // A dialog that will show something (just for the sake of example):
    QDialog *_dialog;
};

#endif 

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

และในครั้งที่สองถ้านายจ้างจะอ่านรหัสเขาจะเข้มงวดกับพอยน์เตอร์ที่ฉันใช้หรือไม่


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

เลียนแบบเส้นแบ่งของคนนี้เนื่องจากมันเกี่ยวกับความหมายเชิงมโนทัศน์ / เจตนาของคลาสเหล่านี้มากพอ ๆ กับเกี่ยวกับรายละเอียดทางเทคนิคของพฤติกรรมและการใช้งาน เนื่องจากคำตอบที่ได้รับการยอมรับโน้มตัวไปทางอดีตฉันมีความสุขที่ได้เป็น "รุ่น PSE" ของคำถาม SO นั้น
Ixrec

คำตอบ:


70

ตัวชี้ "raw" ไม่มีการจัดการ นั่นคือบรรทัดต่อไปนี้:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

... จะทำให้หน่วยความจำรั่วไหลหากdeleteไม่สามารถดำเนินการตามเวลาที่เหมาะสมได้

auto_ptr

เพื่อลดกรณีเหล่านี้std::auto_ptr<>ถูกนำมาใช้ เนื่องจากข้อ จำกัด ของ C ++ ก่อนมาตรฐานปี 2011 แต่ก็ยังง่ายสำหรับauto_ptrการรั่วไหลของหน่วยความจำ มันเพียงพอสำหรับกรณี จำกัด เช่นนี้อย่างไรก็ตาม:

void func() {
    std::auto_ptr<SomeKindOfObject> sKOO_ptr(new SomeKindOfObject());
    // do some work
    // will not leak if you do not copy sKOO_ptr.
}

กรณีการใช้งานที่อ่อนแอที่สุดอย่างหนึ่งอยู่ในคอนเทนเนอร์ นี่เป็นเพราะหากมีการทำสำเนาของauto_ptr<>และสำเนาเก่าไม่ได้ถูกตั้งค่าใหม่อย่างรอบคอบคอนเทนเนอร์อาจลบตัวชี้และข้อมูลสูญหาย

unique_ptr

เพื่อทดแทน C ++ 11 แนะนำ std::unique_ptr<> :

void func2() {
    std::unique_ptr<SomeKindofObject> sKOO_unique(new SomeKindOfObject());

    func3(sKOO_unique); // now func3() owns the pointer and sKOO_unique is no longer valid
}

ดังกล่าวunique_ptr<>จะได้รับการทำความสะอาดอย่างถูกต้องแม้เมื่อมันผ่านไปมาระหว่างฟังก์ชั่น มันทำได้โดยการแสดงความหมายของ "เจ้าของ" ของตัวชี้ - ความหมาย "เจ้าของ" ล้างมัน เหมาะสำหรับใช้ในภาชนะบรรจุ:

std::vector<std::unique_ptr<SomeKindofObject>> sKOO_vector();

ซึ่งแตกต่างจากauto_ptr<>, unique_ptr<>เป็นที่มีความประพฤติดีที่นี่และเมื่อvectorปรับขนาดไม่มีวัตถุที่จะถูกลบโดยไม่ตั้งใจในขณะที่vectorสำเนาเก็บสนับสนุนของมัน

shared_ptr และ weak_ptr

unique_ptr<>มีประโยชน์เพื่อให้แน่ใจ แต่มีบางกรณีที่คุณต้องการให้ส่วนฐานสองส่วนของคุณสามารถอ้างถึงวัตถุเดียวกันและคัดลอกตัวชี้ไปรอบ ๆ ในขณะที่ยังรับประกันการล้างข้อมูลที่เหมาะสม ตัวอย่างเช่นต้นไม้อาจมีลักษณะเช่นนี้เมื่อใช้std::shared_ptr<>:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

ในกรณีนี้เรายังสามารถเก็บสำเนาของโหนดรูทหลายชุดและต้นไม้จะถูกล้างอย่างเหมาะสมเมื่อสำเนาทั้งหมดของโหนดรูทถูกทำลาย

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

ดังนั้นสิ่งนี้จึงแนะนำปัญหา: โครงสร้างที่มีการเชื่อมโยงสองด้านจบลงด้วยการอ้างอิงแบบวงกลม สมมติว่าเราต้องการเพิ่มparentตัวชี้ไปยังต้นไม้ของเราNode:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

ทีนี้ถ้าเราลบ a ออกNodeมันจะมีการอ้างอิงแบบวนรอบ มันจะไม่เป็นdeleted เพราะจำนวนการอ้างอิงของมันจะไม่เป็นศูนย์

ในการแก้ปัญหานี้คุณใช้std::weak_ptr<>:

template<class T>
struct Node {
    T value;
    std::weak_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

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

std::shared_ptr<Node<T>> parent_of_this = node->parent.lock();

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

make_shared และ make_unique

ขณะนี้มีปัญหาเล็กน้อยกับshared_ptr<>และunique_ptr<>ที่ควรได้รับการแก้ไข สองบรรทัดต่อไปนี้มีปัญหา:

foo_unique(std::unique_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());
foo_shared(std::shared_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());

หากthrower()มีข้อผิดพลาดทั้งสองบรรทัดจะทำให้หน่วยความจำรั่ว และยิ่งกว่านั้นshared_ptr<>ให้นับการอ้างอิงอยู่ไกลจากวัตถุที่ชี้ไปและสิ่งนี้ทำได้หมายถึงการจัดสรรครั้งที่สอง) ไม่เป็นที่พึงปรารถนา

C ++ 11 มีstd::make_shared<>()และ C ++ 14 มีให้std::make_unique<>()เพื่อแก้ไขปัญหานี้:

foo_unique(std::make_unique<SomeKindofObject>(), thrower());
foo_shared(std::make_shared<SomeKindofObject>(), thrower());

ในทั้งสองกรณีแม้ว่าจะthrower()มีข้อผิดพลาดเกิดขึ้นจะไม่มีการรั่วไหลของหน่วยความจำ โบนัสmake_shared<>()มีโอกาสสร้างจำนวนการอ้างอิงในพื้นที่หน่วยความจำเดียวกันเดียวกับวัตถุที่มีการจัดการซึ่งสามารถทำได้เร็วกว่าและสามารถบันทึกหน่วยความจำได้สองสามไบต์ในขณะที่ให้การรับประกันความปลอดภัยยกเว้น!

หมายเหตุเกี่ยวกับ Qt

อย่างไรก็ตามควรสังเกตว่า Qt ซึ่งต้องรองรับคอมไพเลอร์ pre-C ++ 11 มีรูปแบบการเก็บขยะของตัวเอง: หลายคนQObjectมีกลไกที่จะถูกทำลายอย่างถูกต้องโดยผู้ใช้ไม่จำเป็นต้องdeleteพวกเขา

ฉันไม่ทราบว่าQObjectจะทำงานอย่างไรเมื่อจัดการโดยตัวชี้ที่มีการจัดการ C ++ 11 ดังนั้นฉันจึงไม่สามารถพูดได้ว่าshared_ptr<QDialog>เป็นความคิดที่ดี ฉันไม่มีประสบการณ์เพียงพอกับ Qt ที่จะพูดอย่างแน่นอน แต่ฉันเชื่อว่า Qt5 ได้รับการปรับสำหรับกรณีการใช้งานนี้


1
@Zilators: โปรดทราบความคิดเห็นที่เพิ่มของฉันเกี่ยวกับ Qt คำตอบสำหรับคำถามของคุณว่าควรจัดการตัวชี้ทั้งสามนั้นขึ้นอยู่กับว่าวัตถุ Qt จะทำงานได้ดีหรือไม่
greyfade

2
"ทั้งสองทำการจัดสรรแยกเพื่อถือตัวชี้" หรือไม่ ไม่ unique_ptr ไม่เคยจัดสรรอะไรเพิ่มเติมเพียง shared_ptr ต้องจัดสรรการอ้างอิง - นับ + allocator- วัตถุ "ทั้งสองสายจะทำให้หน่วยความจำรั่ว" ไม่เพียงแค่อาจไม่รับประกันถึงพฤติกรรมที่ไม่ดี
Deduplicator

1
@Dupuplicator: ข้อความของฉันต้องไม่ชัดเจน: shared_ptrเป็นวัตถุที่แยกต่างหาก - การจัดสรรที่แยกต่างหาก - จากnewวัตถุ ed พวกมันมีอยู่ในสถานที่ต่างกัน make_sharedมีความสามารถในการรวมเข้าด้วยกันในตำแหน่งเดียวกันซึ่งช่วยปรับปรุงตำแหน่งแคชในหมู่สิ่งอื่น ๆ
greyfade

2
@greyfade: Nononono shared_ptrเป็นวัตถุ และการจัดการวัตถุนั้นจะต้องจัดสรร (การอ้างอิงนับ (อ่อนแอ + แข็งแรง) + เรือพิฆาต) -object make_sharedอนุญาตให้จัดสรรและวัตถุที่ถูกจัดการเป็นชิ้นเดียว unique_ptrไม่ได้ใช้สิ่งเหล่านั้นดังนั้นจึงไม่มีข้อได้เปรียบที่สอดคล้องกันนอกเหนือจากการทำให้แน่ใจว่าวัตถุนั้นเป็นของตัวชี้สมาร์ทเสมอ ในฐานะที่เป็นกันคนหนึ่งสามารถมีshared_ptrซึ่งเป็นเจ้าของวัตถุพื้นฐานและเป็นตัวแทนnullptrหรือที่ไม่ได้เป็นเจ้าของและเป็นตัวแทนที่ไม่ใช่ nullpointer
Deduplicator

1
ฉันดูที่มันและดูเหมือนจะมีความสับสนโดยทั่วไปเกี่ยวกับสิ่งที่shared_ptrทำ: 1. มันเป็นเจ้าของวัตถุบางอย่าง (แสดงโดยวัตถุภายในที่จัดสรรแบบไดนามิกซึ่งมีจำนวนการอ้างอิงที่อ่อนแอและแข็งแรงเช่นเดียวกับ deleter) . 2. มันมีตัวชี้ ทั้งสองส่วนนั้นเป็นอิสระ make_uniqueและmake_sharedทั้งสองตรวจสอบให้แน่ใจว่าวางวัตถุที่จัดสรรไว้อย่างปลอดภัยในตัวชี้สมาร์ท นอกจากนี้ยังmake_sharedช่วยให้การจัดสรรความเป็นเจ้าของวัตถุและตัวชี้การจัดการร่วมกัน
Deduplicator
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.