shared_ptr มายากล :)


91

นายLidströmและฉันมีข้อโต้แย้ง :)

คำกล่าวอ้างของMr.Lidströmคือโครงสร้างshared_ptr<Base> p(new Derived);ไม่ต้องการให้ Base มีตัวทำลายเสมือน:

Armen Tsirunyan : "จริงเหรอshared_ptr จะล้างข้อมูลอย่างถูกต้องหรือไม่ในกรณีนี้คุณช่วยสาธิตวิธีการใช้เอฟเฟกต์นั้นได้ไหม"

Daniel Lidström : " shared_ptrใช้ตัวทำลายของตัวเองเพื่อลบอินสแตนซ์คอนกรีตสิ่งนี้เรียกว่า RAII ในชุมชน C ++ คำแนะนำของฉันคือคุณเรียนรู้ทุกอย่างเกี่ยวกับ RAII มันจะทำให้การเข้ารหัส C ++ ของคุณง่ายขึ้นมากเมื่อคุณใช้ RAII ในทุกสถานการณ์ "

Armen Tsirunyan : "ฉันรู้เกี่ยวกับ RAII และฉันก็รู้ด้วยว่าในที่สุดตัวทำลายshared_ptrอาจลบ px ที่เก็บไว้เมื่อ pn ถึง 0 แต่ถ้า px มีตัวชี้ประเภทคงที่Baseและตัวชี้ชนิดไดนามิกเป็นDerivedดังนั้นเว้นแต่จะBaseมีตัวทำลายเสมือน จะส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนดแก้ไขฉันถ้าฉันทำผิด "

Daniel Lidström : " shared_ptrรู้ว่าประเภทคงที่คือคอนกรีตมันรู้เรื่องนี้ตั้งแต่ฉันส่งผ่านมันไปในตัวสร้างมันดูเหมือนเวทมนตร์เล็กน้อย แต่ฉันมั่นใจได้ว่ามันเป็นเพราะการออกแบบและดีมาก"

ดังนั้นตัดสินเรา เป็นไปได้อย่างไร (ถ้าเป็น) ที่จะใช้shared_ptrโดยไม่ต้องใช้คลาส polymorphic ที่มีตัวทำลายเสมือน ขอบคุณล่วงหน้า


3
คุณสามารถเชื่อมโยงกับชุดข้อความเดิมได้
Darin Dimitrov

8
สิ่งที่น่าสนใจอีกอย่างคือมันshared_ptr<void> p(new Derived)จะทำลายDerivedวัตถุด้วยตัวทำลายไม่ว่ามันจะเป็นvirtualหรือไม่ก็ตาม
dalle

7
วิธีถามคำถามที่ยอดเยี่ยม :)
rubenvb

5
แม้ว่า shared_ptr จะอนุญาต แต่ก็เป็นความคิดที่แย่มากที่จะออกแบบคลาสให้เป็นฐานโดยไม่มี dtor เสมือน ความคิดเห็นของ Daniel เกี่ยวกับ RAII ทำให้เข้าใจผิดซึ่งไม่เกี่ยวข้องกับสิ่งนี้ แต่การสนทนาที่ยกมานั้นฟังดูเหมือนเป็นการสื่อสารที่ผิดพลาดง่ายๆ (และสมมติฐานที่ไม่ถูกต้องว่า shared_ptr ทำงานอย่างไร)

6
ไม่ใช่ RAII แต่เป็นการลบตัวทำลาย คุณต้องระวังเพราะshared_ptr<T>( (T*)new U() )ที่ไหนstruct U:Tจะไม่ทำสิ่งที่ถูกต้อง (และสามารถทำได้ทางอ้อมอย่างง่ายดายเช่นฟังก์ชั่นที่รับT*และส่งผ่าน a U*)
Yakk - Adam Nevraumont

คำตอบ:


74

ใช่มันเป็นไปได้ที่จะใช้ shared_ptr ด้วยวิธีนี้ Boost ทำและมาตรฐาน C ++ 11 ก็ต้องการพฤติกรรมนี้เช่นกัน เนื่องจาก shared_ptr มีความยืดหยุ่นที่เพิ่มขึ้นจึงจัดการได้มากกว่าแค่ตัวนับอ้างอิง สิ่งที่เรียกว่า deleter มักถูกใส่ไว้ในบล็อกหน่วยความจำเดียวกันที่มีตัวนับอ้างอิงด้วย แต่ส่วนที่สนุกก็คือประเภทของ deleter นี้ไม่ได้เป็นส่วนหนึ่งของประเภท shared_ptr สิ่งนี้เรียกว่า "type erasure" และโดยพื้นฐานแล้วเป็นเทคนิคเดียวกับที่ใช้สำหรับการปรับใช้ "ฟังก์ชัน polymorphic" boost :: function หรือ std :: function เพื่อซ่อนประเภท functor ที่แท้จริง เพื่อให้ตัวอย่างของคุณใช้งานได้เราจำเป็นต้องมีตัวสร้างเทมเพลต:

template<class T>
class shared_ptr
{
public:
   ...
   template<class Y>
   explicit shared_ptr(Y* p);
   ...
};

ดังนั้นหากคุณใช้สิ่งนี้กับคลาส Base และ Derived ...

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

int main() {
   shared_ptr<Base> sp (new Derived);
}

... ตัวสร้างเทมเพลตที่มี Y = Derived ถูกใช้เพื่อสร้างอ็อบเจ็กต์ shared_ptr ตัวสร้างจึงมีโอกาสที่จะสร้างอ็อบเจ็กต์ deleter และตัวนับการอ้างอิงที่เหมาะสมและจัดเก็บตัวชี้ไปยังบล็อกควบคุมนี้ในฐานะสมาชิกข้อมูล หากตัวนับการอ้างอิงถึงศูนย์จะใช้ deleter ที่สร้างขึ้นก่อนหน้านี้และ Derived-Aware เพื่อกำจัดวัตถุ

มาตรฐาน C ++ 11 มีสิ่งต่อไปนี้เพื่อพูดเกี่ยวกับตัวสร้างนี้ (20.7.2.2.1):

ต้องการ: pต้องสามารถเปลี่ยนเป็นT*. Yจะต้องเป็นประเภทที่สมบูรณ์ การแสดงออกdelete pจะต้องมีรูปแบบที่ดีต้องมีพฤติกรรมที่กำหนดไว้อย่างดีและจะต้องไม่มีข้อยกเว้น

ผลกระทบ:โครงสร้างshared_ptrวัตถุที่เป็นเจ้าของpตัวชี้

และสำหรับผู้ทำลาย (20.7.2.2.2):

ผลกระทบ:ถ้า*thisเป็นที่ว่างเปล่าหรือหุ้นเป็นเจ้าของอีกด้วยshared_ptrตัวอย่างเช่น ( use_count() > 1) มีไม่มีผลข้างเคียง มิฉะนั้นถ้า*thisเป็นเจ้าของวัตถุpและ Deleter d, d(p)ที่เรียกว่า มิฉะนั้นหาก*thisเป็นเจ้าของตัวชี้pและdelete pถูกเรียก

(เน้นโดยใช้แบบอักษรตัวหนาเป็นของฉัน)


the upcoming standard also requires this behaviour: (ก) มาตรฐานใดและ (ข) คุณสามารถระบุข้อมูลอ้างอิงได้ (ถึงมาตรฐาน)?
kevinarpe

ผมแค่อยากจะเพิ่มความคิดเห็นเพื่อ @sellibitze 's add a commentคำตอบเพราะผมไม่ได้มีมากพอที่จะจุด IMO มันเป็นBoost does thisมากกว่าthe Standard requires. ฉันไม่คิดว่ามาตรฐานต้องการสิ่งนั้นจากสิ่งที่ฉันเข้าใจ พูดคุยเกี่ยวกับตัวอย่าง @sellibitze 's shared_ptr<Base> sp (new Derived);, ต้องของconstructorเพียงแค่ขอdelete Derivedเป็นกำหนดไว้อย่างดีและรูปแบบที่ดี สำหรับสเปคของdestructorมันก็มีpเช่นกัน แต่ฉันไม่คิดว่ามันหมายถึงpในข้อกำหนดของconstructor.
Lujun Weng

28

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

ตัวอย่างเช่นคุณสามารถสร้าง deleter ที่กำหนดเอง:

void DeleteDerived(Derived* d) { delete d; } // EDIT: no conversion needed.

shared_ptr<Base> p(new Derived, DeleteDerived);

p จะเรียก DeleteDerived เพื่อทำลายวัตถุปลายแหลม การติดตั้งจะดำเนินการโดยอัตโนมัติ


4
+1 สำหรับข้อสังเกตเกี่ยวกับประเภทที่ไม่สมบูรณ์มีประโยชน์มากเมื่อใช้ a shared_ptrเป็นแอตทริบิวต์
Matthieu M.

16

เพียงแค่

shared_ptr ใช้ฟังก์ชัน deleter พิเศษที่สร้างขึ้นโดยตัวสร้างที่ใช้ตัวทำลายของวัตถุที่กำหนดเสมอไม่ใช่ตัวทำลายของฐานนี่เป็นงานเล็กน้อยกับการเขียนโปรแกรมเมตาเทมเพลต แต่ใช้งานได้

อะไรแบบนั้น

template<typename SomeType>
shared_ptr(SomeType *p)
{
   this->destroyer = destroyer_function<SomeType>(p);
   ...
}

1
อืม ... น่าสนใจฉันเริ่มเชื่อแล้ว :)
Armen Tsirunyan

1
@Armen Tsirunyan คุณควรดูรายละเอียดการออกแบบของ shared_ptr ก่อนที่จะเริ่มอภิปราย 'การจับผู้ทำลาย' นี้เป็นหนึ่งในคุณสมบัติที่สำคัญของ shared_ptr ...
Paul Michalik

6
@ paul_71: ฉันเห็นด้วยกับคุณ ในทางกลับกันฉันเชื่อว่าการสนทนานี้ไม่เพียง แต่มีประโยชน์สำหรับฉัน แต่สำหรับคนอื่น ๆ ที่ไม่ทราบข้อเท็จจริงเกี่ยวกับ shared_ptr นี้ ดังนั้นฉันคิดว่ามันไม่ใช่บาปใหญ่ที่จะเริ่มหัวข้อนี้ต่อไป :)
Armen Tsirunyan

3
@ อาร์เมนไม่แน่นอน แต่คุณทำได้ดีในการชี้ไปที่คุณลักษณะที่สำคัญมากของ shared_ptr <T> ซึ่งมักจะดูแลโดยนักพัฒนา c ++ ที่มีประสบการณ์
Paul Michalik
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.