มีเหตุผลที่ดีไหมที่จะไม่ประกาศตัวทำลายเสมือนสำหรับชั้นเรียน เมื่อใดที่คุณควรหลีกเลี่ยงการเขียนโดยเฉพาะ
มีเหตุผลที่ดีไหมที่จะไม่ประกาศตัวทำลายเสมือนสำหรับชั้นเรียน เมื่อใดที่คุณควรหลีกเลี่ยงการเขียนโดยเฉพาะ
คำตอบ:
ไม่จำเป็นต้องใช้ตัวทำลายเสมือนเมื่อสิ่งใด ๆ ด้านล่างเป็นจริง:
ไม่มีเหตุผลที่เฉพาะเจาะจงในการหลีกเลี่ยงเว้นแต่คุณจะถูกกดทับสำหรับหน่วยความจำจริงๆ
เพื่อตอบคำถามอย่างชัดเจนกล่าวคือเมื่อใดที่คุณไม่ควรประกาศตัวทำลายเสมือน
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
}
ฉันประกาศตัวทำลายเสมือนถ้าฉันมีวิธีการเสมือนจริง เมื่อฉันมีวิธีการเสมือนจริงฉันไม่ไว้วางใจตัวเองที่จะหลีกเลี่ยงการสร้างอินสแตนซ์บนฮีปหรือจัดเก็บตัวชี้ไปยังคลาสพื้นฐาน ทั้งสองอย่างนี้เป็นการดำเนินการที่พบบ่อยมากและมักจะรั่วไหลของทรัพยากรอย่างเงียบ ๆ หากไม่ประกาศตัวทำลายล้าง
จำเป็นต้องมีตัวทำลายเสมือนเมื่อใดก็ตามที่มีโอกาสที่delete
อาจเรียกตัวชี้ไปยังวัตถุของคลาสย่อยที่มีประเภทของคลาสของคุณ สิ่งนี้ทำให้แน่ใจว่าจะเรียกตัวทำลายล้างที่ถูกต้องในขณะรันโดยที่คอมไพเลอร์ไม่ต้องรู้คลาสของอ็อบเจ็กต์บนฮีปในเวลาคอมไพล์ ตัวอย่างเช่นสมมติว่าB
เป็นคลาสย่อยของA
:
A *x = new B;
delete x; // ~B() called, even though x has type A*
หากโค้ดของคุณไม่สำคัญในด้านประสิทธิภาพการเพิ่มตัวทำลายเสมือนให้กับคลาสพื้นฐานทุกคลาสที่คุณเขียนก็เป็นเรื่องที่สมเหตุสมผลเพื่อความปลอดภัย
อย่างไรก็ตามหากคุณพบว่าตัวเองdelete
มีวัตถุจำนวนมากอยู่ในวง จำกัด ประสิทธิภาพเหนือศีรษะของการเรียกใช้ฟังก์ชันเสมือน (แม้จะว่างเปล่าก็ตาม) อาจสังเกตเห็นได้ชัดเจน โดยปกติคอมไพลเลอร์ไม่สามารถแทรกสายเหล่านี้ได้และโปรเซสเซอร์อาจคาดเดาได้ยากว่าจะไปที่ใด ไม่น่าจะมีผลกระทบอย่างมีนัยสำคัญต่อประสิทธิภาพการทำงาน แต่ก็น่ากล่าวถึง
ฟังก์ชันเสมือนหมายถึงทุกออบเจ็กต์ที่จัดสรรเพิ่มขึ้นในต้นทุนหน่วยความจำโดยตัวชี้ตารางฟังก์ชันเสมือน
ดังนั้นหากโปรแกรมของคุณเกี่ยวข้องกับการจัดสรรอ็อบเจ็กต์จำนวนมากคุณควรหลีกเลี่ยงฟังก์ชันเสมือนทั้งหมดเพื่อบันทึก 32 บิตเพิ่มเติมต่อออบเจ็กต์
ในกรณีอื่น ๆ ทั้งหมดคุณจะช่วยตัวเองในการแก้ปัญหาความทุกข์ยากเพื่อทำให้ dtor เสมือนจริง
คลาส 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 หากคุณกำลังส่งพอยน์เตอร์ไปยังออบเจ็กต์ของคลาสนี้นับประสาคลาสย่อยของคลาสนี้คุณกำลังทำผิด
เหตุผลที่ดีในการไม่ประกาศตัวทำลายเป็นเสมือนคือเมื่อสิ่งนี้ช่วยประหยัดคลาสของคุณจากการเพิ่มตารางฟังก์ชันเสมือนและคุณควรหลีกเลี่ยงสิ่งนั้นเมื่อทำได้
ฉันรู้ว่าหลายคนชอบที่จะประกาศว่าผู้ทำลายล้างเป็นเสมือนอยู่เสมอเพื่อที่จะอยู่ในด้านที่ปลอดภัย แต่ถ้าชั้นเรียนของคุณไม่มีฟังก์ชั่นเสมือนจริงอื่น ๆ ก็ไม่มีจุดที่จะมีตัวทำลายเสมือน แม้ว่าคุณจะให้ชั้นเรียนของคุณกับคนอื่น ๆ ที่ได้มาจากคลาสอื่น ๆ แล้วพวกเขาก็ไม่มีเหตุผลที่จะเรียกลบบนตัวชี้ที่อัปโหลดไปยังชั้นเรียนของคุณ - และถ้าพวกเขาทำฉันจะถือว่านี่เป็นข้อบกพร่อง
โอเคมีข้อยกเว้นเพียงข้อเดียวคือถ้าคลาสของคุณ (mis-) ใช้ในการลบโพลีมอร์ฟิกของอ็อบเจ็กต์ที่ได้รับ แต่คุณหรือคนอื่น ๆ - หวังว่าจะรู้ว่าสิ่งนี้ต้องการตัวทำลายเสมือน
พูดอีกอย่างคือถ้าชั้นเรียนของคุณมีตัวทำลายที่ไม่ใช่เสมือนนี่เป็นคำสั่งที่ชัดเจนมาก: "อย่าใช้ฉันเพื่อลบวัตถุที่ได้รับ!"
หากคุณมีคลาสขนาดเล็กมากและมีอินสแตนซ์จำนวนมากค่าใช้จ่ายของตัวชี้ vtable สามารถสร้างความแตกต่างในการใช้หน่วยความจำของโปรแกรมของคุณ ตราบใดที่ชั้นเรียนของคุณไม่มีเมธอดเสมือนอื่น ๆ การทำให้ destructor ไม่ใช่เสมือนจะช่วยประหยัดค่าใช้จ่ายนั้น
ฉันมักจะประกาศว่า destructor virtual แต่ถ้าคุณมีรหัสสำคัญด้านประสิทธิภาพที่ใช้ในวงในคุณอาจต้องการหลีกเลี่ยงการค้นหาตารางเสมือน ซึ่งอาจมีความสำคัญในบางกรณีเช่นการตรวจสอบการชนกัน แต่ระวังว่าคุณทำลายวัตถุเหล่านั้นอย่างไรหากคุณใช้มรดกหรือคุณจะทำลายวัตถุเพียงครึ่งเดียว
โปรดสังเกตว่าการค้นหาตารางเสมือนจะเกิดขึ้นสำหรับออบเจ็กต์หากวิธีการใด ๆบนวัตถุนั้นเป็นเสมือน ดังนั้นไม่มีประเด็นในการลบข้อกำหนดเสมือนบนตัวทำลายหากคุณมีวิธีการเสมือนอื่น ๆ ในคลาส
หากคุณต้องมั่นใจว่าชั้นเรียนของคุณไม่มี vtable คุณก็ต้องไม่มีตัวทำลายเสมือนเช่นกัน
นี่เป็นกรณีที่หายาก แต่ก็เกิดขึ้น
ตัวอย่างรูปแบบที่คุ้นเคยมากที่สุดคือคลาส DirectX D3DVECTOR และ D3DMATRIX นี่คือเมธอดคลาสแทนที่จะเป็นฟังก์ชันสำหรับน้ำตาลวากยสัมพันธ์ แต่คลาสนั้นตั้งใจจะไม่มี vtable เพื่อหลีกเลี่ยงค่าใช้จ่ายของฟังก์ชันเนื่องจากคลาสเหล่านี้ใช้เฉพาะในวงในของแอปพลิเคชันประสิทธิภาพสูงจำนวนมาก
เกี่ยวกับการดำเนินการที่จะดำเนินการในคลาสฐานและที่ควรปฏิบัติจริงควรเป็นเสมือน หากการลบสามารถดำเนินการได้หลายรูปแบบผ่านอินเตอร์เฟสคลาสพื้นฐานการลบจะต้องทำงานเสมือนจริงและเสมือนจริง
ผู้ทำลายไม่จำเป็นต้องเป็นเสมือนหากคุณไม่ได้ตั้งใจที่จะได้มาจากคลาส และแม้ว่าคุณจะทำป้องกัน destructor ไม่ใช่เสมือนเป็นเพียงที่ดีถ้าลบของตัวชี้ชั้นฐานไม่จำเป็นต้องใช้
คำตอบด้านประสิทธิภาพคือคำตอบเดียวที่ฉันรู้ซึ่งมีโอกาสเป็นจริง หากคุณได้วัดและพบว่าการยกเลิกการจำลองเสมือนผู้ทำลายล้างของคุณทำให้สิ่งต่างๆเร็วขึ้นจริง ๆ คุณอาจมีสิ่งอื่น ๆ ในคลาสนั้นที่ต้องเร่งความเร็วเช่นกัน แต่ ณ จุดนี้มีข้อควรพิจารณาที่สำคัญกว่า วันหนึ่งมีคนค้นพบว่าโค้ดของคุณจะให้คลาสพื้นฐานที่ดีสำหรับพวกเขาและช่วยให้พวกเขาทำงานได้หนึ่งสัปดาห์ คุณควรตรวจสอบให้แน่ใจว่าพวกเขาทำงานในสัปดาห์นั้นคัดลอกและวางรหัสของคุณแทนที่จะใช้รหัสของคุณเป็นฐาน คุณควรตรวจสอบให้แน่ใจว่าคุณกำหนดวิธีการสำคัญบางอย่างไว้เป็นส่วนตัวเพื่อที่จะไม่มีใครสืบทอดจากคุณได้