เหตุใดฉันจึงต้องประกาศตัวทำลายเสมือนสำหรับคลาสนามธรรมใน C ++


165

ฉันรู้ว่ามันเป็นแนวปฏิบัติที่ดีในการประกาศ destructors เสมือนจริงสำหรับคลาสพื้นฐานใน C ++ แต่เป็นสิ่งสำคัญเสมอที่จะประกาศvirtualdestructors แม้สำหรับคลาสนามธรรมที่ทำหน้าที่เป็นอินเตอร์เฟสหรือไม่ โปรดระบุเหตุผลและตัวอย่างว่าทำไม

คำตอบ:


196

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

ตัวอย่างเช่น

class Interface
{
   virtual void doSomething() = 0;
};

class Derived : public Interface
{
   Derived();
   ~Derived() 
   {
      // Do some important cleanup...
   }
};

void myFunc(void)
{
   Interface* p = new Derived();
   // The behaviour of the next line is undefined. It probably 
   // calls Interface::~Interface, not Derived::~Derived
   delete p; 
}

4
delete pเรียกใช้พฤติกรรมที่ไม่ได้กำหนด Interface::~Interfaceมันไม่ได้รับประกันว่าจะโทร
Mankarse

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

14
@Wallacoloo: มันจะไม่ได้กำหนดเนื่องจากการ:[expr.delete]/ ... if the static type of the object to be deleted is different from its dynamic type, ... the static type shall have a virtual destructor or the behavior is undefined. ...มันจะยังไม่ได้กำหนดถ้า Derived ใช้ destructor ที่สร้างขึ้นโดยปริยาย
Mankarse

37

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

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

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

[ดูรายการ 4 ในบทความนี้: http://www.gotw.ca/publications/mill18.htm ]


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

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

มิเชลฉันพูดอย่างนั้นแล้ว :) "ถ้าคุณทำอย่างนั้นคุณทำให้ destructor ของคุณได้รับการปกป้องถ้าคุณทำเช่นนั้นลูกค้าจะไม่สามารถลบโดยใช้ตัวชี้ไปยังอินเทอร์เฟซนั้น" และแน่นอนว่ามันไม่ได้พึ่งพาลูกค้า แต่ต้องบังคับให้ลูกค้าบอกว่า "คุณทำไม่ได้ ... " ฉันไม่เห็นอันตรายใด ๆ
Johannes Schaub - litb

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

2
+1 สำหรับการกล่าวถึง destructors ที่ได้รับการป้องกันซึ่งเป็นอีก "ทางออก" ของปัญหาการโทร destructor ผิดโดยไม่ตั้งใจเมื่อลบตัวชี้ไปยังคลาสพื้นฐาน
j_random_hacker

23

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

  1. คลาสของคุณตั้งใจจะใช้เป็นคลาสพื้นฐานหรือไม่?
    • ไม่มี: ประกาศ destructor ไม่ใช่เสมือนของประชาชนที่จะหลีกเลี่ยง V-ชี้บนวัตถุของคลาสแต่ละ*
    • ใช่: อ่านคำถามต่อไป
  2. คลาสพื้นฐานของคุณเป็นนามธรรมหรือไม่ (เช่นวิธีการใด ๆ เสมือนบริสุทธิ์?)
    • ไม่: พยายามทำให้คลาสพื้นฐานของคุณเป็นนามธรรมโดยการออกแบบลำดับชั้นของคลาสใหม่
    • ใช่: อ่านคำถามต่อไป
  3. คุณต้องการอนุญาตให้ลบ polymorphic ผ่านตัวชี้พื้นฐานหรือไม่
    • ไม่: ประกาศตัวทำลายเสมือน destructor เพื่อป้องกันการใช้งานที่ไม่พึงประสงค์
    • ใช่: ประกาศตัวทำลายเสมือนสาธารณะ (ไม่มีค่าใช้จ่ายในกรณีนี้)

ฉันหวังว่านี่จะช่วยได้.

*เป็นเรื่องสำคัญที่จะต้องทราบว่าไม่มีวิธีใดใน C ++ ที่จะทำเครื่องหมายคลาสว่าสุดท้าย (เช่นไม่ใช่ subclassable) ดังนั้นในกรณีที่คุณตัดสินใจที่จะประกาศ destructor ของคุณที่ไม่ใช่เสมือนและสาธารณะอย่าลืมเตือนโปรแกรมเมอร์ของคุณให้ชัดเจน มาจากชั้นเรียนของคุณ

อ้างอิง:


11
คำตอบนี้ล้าสมัยบางส่วนขณะนี้มีคำหลักสุดท้ายใน C ++
Étienne

10

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

ดังนั้นคุณกำลังเปิดโอกาสให้หน่วยความจำรั่ว

class IFoo
{
  public:
    virtual void DoFoo() = 0;
};

class Bar : public IFoo
{
  char* dooby = NULL;
  public:
    virtual void DoFoo() { dooby = new char[10]; }
    void ~Bar() { delete [] dooby; }
};

IFoo* baz = new Bar();
baz->DoFoo();
delete baz; // memory leak - dooby isn't deleted

จริงในความเป็นจริงในตัวอย่างนั้นมันอาจไม่เพียง แต่หน่วยความจำรั่ว แต่อาจผิดพลาด: - /
Evan Teran

7

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

ตัวอย่างเช่น:

Base *p = new Derived;
// use p as you see fit
delete p;

เป็นรูปแบบที่ไม่ดีถ้าBaseไม่ได้ destructor Base *เสมือนเพราะมันจะพยายามที่จะลบวัตถุที่เป็นว่ามันเป็น


คุณไม่ต้องการแก้ไขบูสต์ :: shared_pointer p (Derived ใหม่) เพื่อให้ดูเหมือนบูสต์ :: shared_pointer <Base> p (Derived ใหม่); ? อาจจะ ppl จะเข้าใจคำตอบของคุณแล้วและโหวต
โยฮันเนส Schaub - litb

แก้ไข: "แก้ไข" สองส่วนเพื่อให้มองเห็นวงเล็บเหลี่ยมตามที่ litb แนะนำ
j_random_hacker

@EvanTeran: ผมไม่แน่ใจว่าเรื่องนี้มีการเปลี่ยนแปลงตั้งแต่คำตอบที่ถูกโพสต์ (เอกสาร Boost ที่boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htmแสดงให้เห็นว่ามันอาจจะมี) แต่มันไม่เป็นความจริง วันนี้ที่shared_ptrจะพยายามลบวัตถุราวกับว่ามันเป็นBase *- มันจำประเภทของสิ่งที่คุณสร้างขึ้นด้วย ดูลิงก์อ้างอิงโดยเฉพาะอย่างยิ่งบิตที่ระบุว่า "destructor จะเรียกการลบด้วยตัวชี้เดียวกันพร้อมด้วยประเภทดั้งเดิมแม้ว่า T จะไม่มี virtual destructor หรือเป็นโมฆะ"
Stuart Golodetz

@ StuartGolodetz: อืมคุณอาจพูดถูก แต่ฉันก็ไม่แน่ใจ มันอาจจะยังคงป่วยอยู่ในบริบทนี้เนื่องจากไม่มีตัวทำลายเสมือน มันคุ้มค่าที่จะดูผ่าน
Evan Teran

@EvanTeran: ในกรณีที่มันเป็นประโยชน์ - stackoverflow.com/questions/3899790/shared-ptr-magic
Stuart Golodetz

5

ไม่ใช่แค่การฝึกฝนที่ดีเท่านั้น มันเป็นกฎ # 1 สำหรับลำดับชั้นใด ๆ

  1. คลาสพื้นฐานส่วนใหญ่ของลำดับชั้นใน C ++ จะต้องมีตัวทำลายเสมือน

ตอนนี้สำหรับทำไม ใช้ลำดับชั้นสัตว์ทั่วไป destructors เสมือนต้องผ่านการจัดส่งเสมือนเช่นเดียวกับการเรียกใช้เมธอดอื่น ๆ นำตัวอย่างต่อไปนี้

Animal* pAnimal = GetAnimal();
delete pAnimal;

สมมติว่า Animal เป็นคลาสนามธรรม วิธีเดียวที่ C ++ รู้ว่า destructor ที่เหมาะสมในการโทรคือผ่านการจัดส่งเมธอดเสมือน ถ้า destructor ไม่ใช่เสมือนจริงมันก็จะเรียก Animal destructor และไม่ทำลายวัตถุใด ๆ ในคลาสที่ได้รับมา

เหตุผลในการสร้าง destructor เสมือนในคลาสพื้นฐานคือมันเพียงลบตัวเลือกจากคลาสที่ได้รับ destructor ของพวกเขากลายเป็นเสมือนจริงโดยค่าเริ่มต้น


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

@j_random_hacker ทำให้มันได้รับการคุ้มครองจะไม่ปกป้องคุณจากการลบภายในที่ไม่ถูกต้อง
JaredPar

1
@ JaredPar: ถูกต้อง แต่อย่างน้อยคุณก็สามารถรับผิดชอบในรหัสของคุณเอง - สิ่งที่ยากคือต้องแน่ใจว่ารหัสลูกค้าไม่สามารถทำให้รหัสของคุณระเบิดได้ (ในทำนองเดียวกันการทำส่วนตัวข้อมูลสมาชิกไม่ได้ป้องกันรหัสภายในจากการทำอะไรโง่กับสมาชิกในที่.)
j_random_hacker

@j_random_hacker ขอโทษที่ตอบกลับด้วยโพสต์บล็อก แต่มันเหมาะกับสถานการณ์นี้จริงๆ blogs.msdn.com/jaredpar/archive/2008/03/24/…
JaredPar

@ JaredPar: โพสต์ที่ยอดเยี่ยมฉันเห็นด้วยกับคุณ 100% โดยเฉพาะอย่างยิ่งเกี่ยวกับการตรวจสอบสัญญาในรหัสค้าปลีก ฉันแค่หมายความว่ามีบางกรณีที่คุณรู้ว่าคุณไม่ต้องการ dtor เสมือน ตัวอย่าง: คลาสแท็กสำหรับการแจกจ่ายแม่แบบ มีขนาด 0 คุณใช้การสืบทอดเพื่อบ่งชี้เฉพาะ
j_random_hacker

3

คำตอบนั้นง่ายคุณต้องเป็นเสมือนมิฉะนั้นคลาสฐานจะไม่เป็นคลาส polymorphic ที่สมบูรณ์

    Base *ptr = new Derived();
    delete ptr; // Here the call order of destructors: first Derived then Base.

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

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