เมื่อใดที่คุณไม่ควรใช้ตัวทำลายเสมือน


คำตอบ:


72

ไม่จำเป็นต้องใช้ตัวทำลายเสมือนเมื่อสิ่งใด ๆ ด้านล่างเป็นจริง:

  • ไม่มีความตั้งใจที่จะได้รับคลาสจากมัน
  • ไม่มีการสร้างอินสแตนซ์บนฮีป
  • ไม่มีความตั้งใจที่จะจัดเก็บในตัวชี้ของซูเปอร์คลาส

ไม่มีเหตุผลที่เฉพาะเจาะจงในการหลีกเลี่ยงเว้นแต่คุณจะถูกกดทับสำหรับหน่วยความจำจริงๆ


25
นี่ไม่ใช่คำตอบที่ดี "ไม่มีความจำเป็น" แตกต่างจาก "ไม่ควร" และ "ไม่มีเจตนา" ต่างจาก "ทำไม่ได้"
โปรแกรมเมอร์ Windows

5
เพิ่ม: ไม่มีความตั้งใจที่จะลบอินสแตนซ์ผ่านตัวชี้คลาสพื้นฐาน
Adam Rosenfield

9
นี่ตอบคำถามไม่ได้จริงๆ เหตุผลที่ดีของคุณที่จะไม่ใช้ virtual dtor อยู่ที่ไหน?
mxcl

9
ฉันคิดว่าเมื่อไม่จำเป็นต้องทำอะไรสักอย่างนั่นเป็นเหตุผลที่ดีที่จะไม่ทำ เป็นไปตามหลักการออกแบบอย่างง่ายของ XP
ก.ย.

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

68

เพื่อตอบคำถามอย่างชัดเจนกล่าวคือเมื่อใดที่คุณไม่ควรประกาศตัวทำลายเสมือน

C ++ '98 / '03

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

struct A {
  // virtual ~A ();
  int i;
  int j;
};
void foo () { 
  A a = { 0, 1 };  // Will fail if virtual dtor declared
}

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

void bar (...);
void foo (A & a) { 
  bar (a);  // Undefined behavior if virtual dtor declared
}

[* ประเภท POD คือประเภทที่มีการรับประกันเฉพาะเกี่ยวกับรูปแบบหน่วยความจำ มาตรฐานบอกเพียงว่าหากคุณคัดลอกจากวัตถุที่มีประเภท POD ไปยังอาร์เรย์ของอักขระ (หรืออักขระที่ไม่ได้ลงชื่อ) และกลับมาอีกครั้งผลลัพธ์จะเหมือนกับวัตถุดั้งเดิม]

C ++ ที่ทันสมัย

ในเวอร์ชันล่าสุดของ C ++ แนวคิดของ POD ถูกแบ่งระหว่างเลย์เอาต์คลาสและโครงสร้างการคัดลอกและการทำลาย

สำหรับกรณีจุดไข่ปลามันไม่ใช่พฤติกรรมที่ไม่ได้กำหนดอีกต่อไปตอนนี้ได้รับการสนับสนุนแบบมีเงื่อนไขด้วยความหมายที่นำไปใช้งาน (N3937 - ~ C ++ '14 - 5.2.2 / 7):

... การส่งผ่านอาร์กิวเมนต์ประเภทคลาสที่ประเมินได้ (ข้อ 9) ที่มีตัวสร้างการคัดลอกที่ไม่สำคัญตัวสร้างการย้ายที่ไม่สำคัญหรือตัวทำลายที่ไม่สำคัญโดยไม่มีพารามิเตอร์ที่เกี่ยวข้องได้รับการสนับสนุนตามเงื่อนไขด้วยการใช้งาน - ความหมายที่กำหนด

การประกาศผู้ทำลายล้างนอกจาก=defaultจะหมายความว่ามันไม่สำคัญ (12.4 / 5)

... ตัวทำลายเป็นเรื่องเล็กน้อยหากไม่ได้ให้โดยผู้ใช้ ...

การเปลี่ยนแปลงอื่น ๆ ใน Modern C ++ ช่วยลดผลกระทบของปัญหาการเริ่มต้นรวมในขณะที่ตัวสร้างสามารถเพิ่มได้:

struct A {
  A(int i, int j);
  virtual ~A ();
  int i;

  int j;
};
void foo () { 
  A a = { 0, 1 };  // OK
}

1
คุณพูดถูกและฉันคิดผิดการแสดงไม่ใช่เหตุผลเดียว แต่นี่แสดงให้เห็นว่าฉันพูดถูกเกี่ยวกับส่วนที่เหลือ: โปรแกรมเมอร์ของชั้นเรียนมีรหัสที่ดีกว่าเพื่อป้องกันไม่ให้ชั้นเรียนถูกสืบทอดโดยคนอื่น
โปรแกรมเมอร์ Windows

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

1
@JohnSmith ฉันได้อัปเดตคำตอบแล้ว หวังว่านี่จะช่วยได้
Richard Corden

28

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


3
และในความเป็นจริงมีตัวเลือกคำเตือนบน gcc ซึ่งเตือนในกรณีนั้นอย่างแม่นยำ (วิธีการเสมือนจริง แต่ไม่มี dtor เสมือน)
CesarB

6
แล้วคุณจะไม่เสี่ยงต่อการรั่วไหลของหน่วยความจำถ้าคุณได้รับจากคลาสโดยไม่คำนึงว่าคุณจะมีฟังก์ชันเสมือนอื่น ๆ หรือไม่?
Mag Roader

1
ฉันเห็นด้วยกับแม็ก การใช้ตัวทำลายเสมือนและหรือวิธีการเสมือนนี้เป็นข้อกำหนดแยกกัน ตัวทำลายเสมือนให้ความสามารถสำหรับคลาสในการล้างข้อมูล (เช่นลบหน่วยความจำปิดไฟล์ ฯลฯ ... ) และยังช่วยให้มั่นใจว่าตัวสร้างของสมาชิกทั้งหมดจะถูกเรียก
user48956

7

จำเป็นต้องมีตัวทำลายเสมือนเมื่อใดก็ตามที่มีโอกาสที่deleteอาจเรียกตัวชี้ไปยังวัตถุของคลาสย่อยที่มีประเภทของคลาสของคุณ สิ่งนี้ทำให้แน่ใจว่าจะเรียกตัวทำลายล้างที่ถูกต้องในขณะรันโดยที่คอมไพเลอร์ไม่ต้องรู้คลาสของอ็อบเจ็กต์บนฮีปในเวลาคอมไพล์ ตัวอย่างเช่นสมมติว่าBเป็นคลาสย่อยของA:

A *x = new B;
delete x;     // ~B() called, even though x has type A*

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

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


"หากโค้ดของคุณไม่สำคัญในด้านประสิทธิภาพการเพิ่มตัวทำลายเสมือนให้กับคลาสพื้นฐานทุกคลาสที่คุณเขียนก็เป็นเรื่องที่สมเหตุสมผลเพื่อความปลอดภัย" ควรเน้นมากขึ้นในทุกคำตอบที่ฉันเห็น
csguy

5

ฟังก์ชันเสมือนหมายถึงทุกออบเจ็กต์ที่จัดสรรเพิ่มขึ้นในต้นทุนหน่วยความจำโดยตัวชี้ตารางฟังก์ชันเสมือน

ดังนั้นหากโปรแกรมของคุณเกี่ยวข้องกับการจัดสรรอ็อบเจ็กต์จำนวนมากคุณควรหลีกเลี่ยงฟังก์ชันเสมือนทั้งหมดเพื่อบันทึก 32 บิตเพิ่มเติมต่อออบเจ็กต์

ในกรณีอื่น ๆ ทั้งหมดคุณจะช่วยตัวเองในการแก้ปัญหาความทุกข์ยากเพื่อทำให้ dtor เสมือนจริง


1
เพียงแค่เลือกไนท์ แต่ทุกวันนี้ตัวชี้มักจะเป็น 64 บิตแทนที่จะเป็น 32
Head Geek

5

คลาส C ++ บางคลาสไม่เหมาะสำหรับใช้เป็นคลาสพื้นฐานที่มีความหลากหลายแบบไดนามิก

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

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

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

class MutexLock {
    mutex *mtx_;
public:
    explicit MutexLock(mutex *mtx) : mtx_(mtx) { mtx_->lock(); }
    ~MutexLock() { mtx_->unlock(); }
private:
    MutexLock(const MutexLock &rhs);
    MutexLock &operator=(const MutexLock &rhs);
};

จุดรวมของคลาสนี้คือการนั่งสแต็กสำหรับ RAII หากคุณกำลังส่งพอยน์เตอร์ไปยังออบเจ็กต์ของคลาสนี้นับประสาคลาสย่อยของคลาสนี้คุณกำลังทำผิด


2
การใช้โพลีมอร์ฟิกไม่ได้หมายความถึงการลบโพลีมอร์ฟิก มีกรณีการใช้งานมากมายสำหรับคลาสที่มีเมธอดเสมือน แต่ไม่มีตัวทำลายเสมือน พิจารณากล่องโต้ตอบที่กำหนดแบบคงที่โดยทั่วไปในชุดเครื่องมือ GUI ใด ๆ หน้าต่างหลักจะทำลายออบเจ็กต์ลูกและทราบประเภทที่แน่นอนของแต่ละรายการ แต่หน้าต่างลูกทั้งหมดจะถูกใช้ในหลาย ๆ ที่เช่นการทดสอบการตีการวาดภาพ API การเข้าถึงที่ดึงข้อความสำหรับข้อความ - เครื่องมือในการพูด ฯลฯ
Ben Voigt

4
จริง แต่ผู้ถามถามว่าเมื่อใดที่คุณควรหลีกเลี่ยงตัวทำลายเสมือนโดยเฉพาะ สำหรับกล่องโต้ตอบที่คุณอธิบายตัวทำลายเสมือนไม่มีจุดหมาย แต่ IMO ไม่เป็นอันตราย ฉันไม่แน่ใจว่าฉันมั่นใจว่าฉันไม่จำเป็นต้องลบกล่องโต้ตอบโดยใช้ตัวชี้คลาสพื้นฐาน - ตัวอย่างเช่นในอนาคตฉันอาจต้องการให้หน้าต่างพาเรนต์สร้างวัตถุลูกโดยใช้โรงงาน ดังนั้นจึงไม่ใช่คำถามในการหลีกเลี่ยงตัวทำลายเสมือนเพียงแค่ว่าคุณอาจไม่ต้องกังวลกับการมี ตัวทำลายเสมือนในคลาสที่ไม่เหมาะสำหรับการสร้างมานั้นเป็นอันตรายเพราะมันทำให้เข้าใจผิด
Steve Jessop

4

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

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

โอเคมีข้อยกเว้นเพียงข้อเดียวคือถ้าคลาสของคุณ (mis-) ใช้ในการลบโพลีมอร์ฟิกของอ็อบเจ็กต์ที่ได้รับ แต่คุณหรือคนอื่น ๆ - หวังว่าจะรู้ว่าสิ่งนี้ต้องการตัวทำลายเสมือน

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


3

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


1

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

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


1

หากคุณต้องมั่นใจว่าชั้นเรียนของคุณไม่มี vtable คุณก็ต้องไม่มีตัวทำลายเสมือนเช่นกัน

นี่เป็นกรณีที่หายาก แต่ก็เกิดขึ้น

ตัวอย่างรูปแบบที่คุ้นเคยมากที่สุดคือคลาส DirectX D3DVECTOR และ D3DMATRIX นี่คือเมธอดคลาสแทนที่จะเป็นฟังก์ชันสำหรับน้ำตาลวากยสัมพันธ์ แต่คลาสนั้นตั้งใจจะไม่มี vtable เพื่อหลีกเลี่ยงค่าใช้จ่ายของฟังก์ชันเนื่องจากคลาสเหล่านี้ใช้เฉพาะในวงในของแอปพลิเคชันประสิทธิภาพสูงจำนวนมาก


0

เกี่ยวกับการดำเนินการที่จะดำเนินการในคลาสฐานและที่ควรปฏิบัติจริงควรเป็นเสมือน หากการลบสามารถดำเนินการได้หลายรูปแบบผ่านอินเตอร์เฟสคลาสพื้นฐานการลบจะต้องทำงานเสมือนจริงและเสมือนจริง

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


-7

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


ความหลากหลายจะทำให้สิ่งต่างๆช้าลงอย่างแน่นอน เปรียบเทียบกับสถานการณ์ที่เราต้องการความหลากหลายและเลือกที่จะไม่ทำมันจะช้ากว่า ตัวอย่าง: เราใช้ตรรกะทั้งหมดที่ตัวทำลายคลาสพื้นฐานโดยใช้ RTTI และคำสั่งสวิตช์เพื่อล้างทรัพยากร
ก.ย.

1
ใน C ++ ไม่ใช่ความรับผิดชอบของคุณที่จะหยุดฉันที่สืบทอดจากคลาสของคุณที่คุณได้บันทึกไว้ว่าไม่เหมาะสำหรับใช้เป็นคลาสพื้นฐาน เป็นความรับผิดชอบของฉันที่จะใช้มรดกด้วยความระมัดระวัง เว้นแต่ไกด์สไตล์บ้านจะบอกเป็นอย่างอื่นแน่นอน
Steve Jessop

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