เมื่อใดที่จะไม่ใช้ destructors เสมือน?


48

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

ถ้าอย่างนั้นคำถามคือทำไม c ++ ไม่ได้ตั้งค่า destructors ทั้งหมดโดยค่าเริ่มต้น? หรือในคำถามอื่น ๆ :

เมื่อใดที่ฉันไม่ต้องการใช้ destructors เสมือน?

ในกรณีใดฉันไม่ควรใช้ destructors เสมือน?

ค่าใช้จ่ายในการใช้ destructors เสมือนจริงคืออะไรหากฉันใช้แม้ว่าจะไม่จำเป็นก็ตาม


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

4
นอกจากนี้ฉันคิดว่าในกรณีส่วนใหญ่ผู้ทำลายระบบจำเป็นต้องเป็นเสมือนจริง Nope ไม่ใช่เลย. เฉพาะผู้ที่ละเมิดมรดก (มากกว่าองค์ประกอบนิยม) คิดอย่างนั้น ฉันเห็นแอปพลิเคชั่นทั้งหมดโดยมีคลาสพื้นฐานเพียงเล็กน้อยและฟังก์ชันเสมือนจริง
Matthieu M.

1
@underscore_d ด้วยการใช้งานทั่วไปจะมีรหัสพิเศษที่สร้างขึ้นสำหรับคลาส polymorphic ใด ๆ เว้นแต่ว่าสิ่งที่มีนัยโดยนัยทั้งหมดได้รับการพัฒนาแบบเสมือนจริงและปรับให้เหมาะสม ภายใน ABIs ทั่วไปสิ่งนี้เกี่ยวข้องกับ vtable อย่างน้อยหนึ่งรายการสำหรับแต่ละชั้นเรียน เลย์เอาต์ของคลาสก็จะต้องมีการเปลี่ยนแปลงด้วย คุณไม่สามารถกลับมาใช้ได้อย่างน่าเชื่อถือเมื่อคุณเผยแพร่คลาสดังกล่าวเป็นส่วนหนึ่งของส่วนต่อประสานสาธารณะบางส่วนเนื่องจากการเปลี่ยนแปลงอีกครั้งจะทำให้ความเข้ากันได้ของ ABI ลดลงเนื่องจากเห็นได้ชัดว่าแย่ (ถ้าเป็นไปได้)
FrankHB

1
@underscore_d วลี "at compile time" ไม่ถูกต้อง แต่ฉันคิดว่านี่หมายถึง destructor เสมือนไม่สำคัญหรือไม่ได้ระบุ constexpr ดังนั้นการสร้างรหัสพิเศษนั้นยากที่จะหลีกเลี่ยง (เว้นแต่คุณจะหลีกเลี่ยงการทำลายวัตถุดังกล่าวทั้งหมด) มันจะเป็นอันตรายต่อประสิทธิภาพรันไทม์มากขึ้นหรือน้อยลง
FrankHB

2
@underscore_d "ตัวชี้" ดูเหมือนว่าปลาเฮอริ่งแดง มันอาจจะเป็นตัวชี้ไปยังสมาชิก (ซึ่งไม่ใช่ตัวชี้ตามคำนิยาม) ด้วย ABIs ปกติตัวชี้ไปยังสมาชิกมักจะไม่พอดีกับคำศัพท์ของเครื่อง (เหมือนกับตัวชี้ทั่วไป) และการเปลี่ยนคลาสจาก non-polymorphic เป็น polymorphic มักจะเปลี่ยนขนาดของตัวชี้เป็นสมาชิกของคลาสนี้
FrankHB

คำตอบ:


41

ถ้าคุณเพิ่ม destructor เสมือนเข้ากับคลาส:

  • ในการใช้งาน C ++ ในปัจจุบันส่วนใหญ่วัตถุทุกอินสแตนซ์ของคลาสนั้นจำเป็นต้องเก็บตัวชี้ไปที่ตารางการแจกจ่ายเสมือนสำหรับประเภทรันไทม์และตารางการแจกจ่ายเสมือนนั้นเพิ่มเข้ากับอิมเมจที่ปฏิบัติการได้

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

  • มีตัวชี้เสมือนฝังตัวทำลายการสร้างคลาสด้วยรูปแบบหน่วยความจำที่ตรงกับรูปแบบอินพุตหรือเอาท์พุตที่รู้จัก (เช่นดังนั้น a Price_Tick*สามารถถูกเล็งโดยตรงไปที่หน่วยความจำที่จัดตำแหน่งอย่างเหมาะสมในแพ็คเก็ต UDP ขาเข้าและใช้ในการแยก / เข้าถึงหรือแก้ไขข้อมูลหรือ การจัดวางnewคลาสเพื่อเขียนข้อมูลลงในแพ็คเก็ตขาออก)

  • destructor ที่เรียกตัวเองว่า - ภายใต้เงื่อนไขบางประการ - จะต้องถูกส่งอย่างแท้จริงและดังนั้นจึงไม่อยู่ในสายงานในขณะที่ destructor ที่ไม่ใช่เสมือนอาจ inlining หรือปรับให้เหมาะสมถ้าไม่สำคัญหรือไม่เกี่ยวข้องกับผู้โทร

การโต้แย้ง "ไม่ได้รับการออกแบบให้สืบทอดมาจาก" จะไม่ใช่เหตุผลในทางปฏิบัติสำหรับการไม่มี destructor เสมือนจริงหากไม่ได้เลวร้ายลงในวิธีการปฏิบัติตามที่อธิบายไว้ข้างต้น แต่ให้มันจะเลวร้ายยิ่งที่เป็นเกณฑ์สำคัญสำหรับเมื่อจะต้องจ่ายค่าใช้จ่าย: เริ่มต้นที่จะมี destructor เสมือนถ้าชั้นเรียนของคุณมีความหมายที่จะใช้เป็นชั้นฐาน ไม่จำเป็นเสมอไป แต่จะช่วยให้มั่นใจได้ว่าคลาสใน heirarchy นั้นสามารถใช้งานได้อย่างอิสระมากขึ้นโดยไม่ต้องมีพฤติกรรมที่ไม่ได้ตั้งใจถ้าตัวทำลายคลาสที่ได้รับมาถูกเรียกใช้โดยใช้ตัวชี้หรือการอ้างอิงคลาสพื้นฐาน

"ในกรณีส่วนใหญ่ destructors ต้องเป็นเสมือน"

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


23

ในกรณีใดฉันไม่ควรใช้ destructors เสมือน?

  1. สำหรับคลาสที่เป็นรูปธรรมซึ่งไม่ต้องการถูกสืบทอด
  2. สำหรับคลาสพื้นฐานที่ไม่มีการลบ polymorphic ไคลเอนต์ใดไม่ควรลบ polymorphically โดยใช้ตัวชี้ไปยังฐาน

BTW,

ในกรณีใดควรใช้ destructors เสมือน?

สำหรับคลาสพื้นฐานที่มีการลบ polymorphic


7
+1 สำหรับ # 2 โดยเฉพาะโดยไม่ต้องลบ polymorphic หาก destructor ของคุณไม่สามารถเรียกใช้ผ่านตัวชี้ฐานทำให้เป็นเสมือนจริงโดยไม่จำเป็นและซ้ำซ้อนโดยเฉพาะอย่างยิ่งถ้าคลาสของคุณไม่ได้เป็นเสมือนมาก่อน (ดังนั้นจึงกลายเป็น RTTI ใหม่) เพื่อป้องกันผู้ใช้ที่ละเมิดสิ่งนี้ตามที่ Herb Sutter แนะนำไว้คุณต้องทำให้ dtor ได้รับการปกป้องและไม่ใช่เสมือนของคลาสพื้นฐานเพื่อที่จะสามารถเรียกใช้โดย / หลังจาก destructor ที่ได้รับ
underscore_d

@underscore_d IMHO ที่เป็นจุดสำคัญที่ฉันพลาดในคำตอบเช่นเดียวกับในการปรากฏตัวของมรดกเพียงกรณีที่ฉันไม่จำเป็นต้องนวกรรมิกเสมือนเมื่อฉันสามารถตรวจสอบให้แน่ใจว่ามันไม่จำเป็น
formerlyknownas_463035818

14

ค่าใช้จ่ายในการใช้ destructors เสมือนจริงคืออะไรหากฉันใช้แม้ว่าจะไม่จำเป็นก็ตาม

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

struct Integer
{
    virtual ~Integer() {}
    int value;
};

ในกรณีนี้ราคาหน่วยความจำค่อนข้างใหญ่ ขนาดหน่วยความจำจริงของอินสแตนซ์ของชั้นเรียนมักจะมีลักษณะเช่นนี้ในสถาปัตยกรรม 64 บิต:

struct Integer
{
    // 8 byte vptr overhead
    int value; // 4 bytes
    // typically 4 more bytes of padding for alignment of vptr
};

ผลรวมคือ 16 ไบต์สำหรับIntegerคลาสนี้ซึ่งตรงข้ามกับเพียง 4 ไบต์ หากเราเก็บหนึ่งล้านเหล่านี้ไว้ในอาเรย์เราจะจบลงด้วยการใช้หน่วยความจำ 16 เมกะไบต์: ขนาดแคชซีพียู L3 8 เมกะไบต์สองเท่าและการวนซ้ำในอาเรย์ซ้ำหลายครั้งอาจช้ากว่า 4 เมกะไบต์เทียบเท่า โดยไม่มีตัวชี้เสมือนอันเป็นผลมาจากการเพิ่มแคชและความผิดพลาดของหน้า

อย่างไรก็ตามราคาตัวชี้เสมือนต่อวัตถุนั้นไม่เพิ่มขึ้นเมื่อใช้ฟังก์ชันเสมือนจริงมากขึ้น คุณสามารถมีฟังก์ชันสมาชิกเสมือนได้ 100 ฟังก์ชันในคลาสและค่าใช้จ่ายต่ออินสแตนซ์จะยังคงเป็นตัวชี้เสมือนเดียว

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

นักพัฒนา Java ที่ทำงานในพื้นที่ที่มีประสิทธิภาพสูงเข้าใจค่าใช้จ่ายประเภทนี้ค่อนข้างดี (แม้ว่ามักจะอธิบายไว้ในบริบทของการชกมวย) เนื่องจากผู้ใช้ที่กำหนดโดยจาวาประเภท Java ที่สืบทอดโดยปริยายจากobjectคลาสฐานกลางและฟังก์ชั่นทั้งหมดใน Java ) ในลักษณะเว้นแต่จะระบุไว้เป็นอย่างอื่น ดังนั้น Java Integerจึงมีแนวโน้มที่จะต้องการหน่วยความจำ 16 ไบต์บนแพลตฟอร์ม 64 บิตเนื่องจากvptrเมตาดาต้าสไตล์นี้เชื่อมโยงกับอินสแตนซ์และโดยทั่วไปแล้วมันเป็นไปไม่ได้ที่จาวาจะห่อสิ่งใดสิ่งหนึ่งintลงในชั้นเรียน ต้นทุนประสิทธิภาพสำหรับมัน

ถ้าอย่างนั้นคำถามคือทำไม c ++ ไม่ได้ตั้งค่า destructors ทั้งหมดโดยค่าเริ่มต้น?

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

เมื่อใดที่ฉันไม่ต้องการใช้ destructors เสมือน?

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

class BaseClass
{
protected:
    // Disallow deleting/destroying subclass objects through `BaseClass*`.
    ~BaseClass() {}
};

ในกรณีใดฉันไม่ควรใช้ destructors เสมือน?

จริง ๆ แล้วมันง่ายกว่าที่จะครอบคลุมเมื่อคุณควรใช้ destructors เสมือน บ่อยครั้งที่คลาสที่มากขึ้นใน codebase ของคุณจะไม่ได้รับการออกแบบสำหรับการสืบทอด

std::vectorตัวอย่างเช่นไม่ได้ถูกออกแบบมาให้สืบทอดและโดยทั่วไปไม่ควรสืบทอด (การออกแบบที่สั่นคลอนมาก) เนื่องจากจะทำให้เกิดปัญหาการลบตัวชี้ฐานนี้ ( std::vectorจงใจหลีกเลี่ยงการทำลายเสมือนจริง) โดยเจตนานอกเหนือจากปัญหาการแบ่งวัตถุที่เงอะงะคลาสที่ได้รับจะเพิ่มสถานะใหม่ใด ๆ

โดยทั่วไปคลาสที่สืบทอดควรมี destructor เสมือนสาธารณะหรือที่ได้รับการป้องกัน จากC++ Coding Standardsตอนที่ 50:

50. ทำให้ตัวทำลายคลาสพื้นฐานเป็นสาธารณะและเสมือนหรือได้รับการป้องกันและไม่ใช่เสมือน หากต้องการลบหรือไม่ลบ; นั่นคือคำถาม: หากการลบผ่านตัวชี้ไปยังฐานฐานควรได้รับอนุญาตจากนั้น destructor ของฐานจะต้องเป็นสาธารณะและเสมือน มิฉะนั้นควรได้รับการปกป้องและไม่เป็นเสมือน

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

ในกรณีที่คุณต้องการรับมรดกเพื่อนำโค้ดที่มีอยู่กลับมาใช้บ่อยครั้งการส่งเสริมการแต่งเพลงก็มักจะได้รับการสนับสนุนอย่างมาก (Composite Reuse Principle)


9

ทำไม c ++ ไม่ได้ตั้งค่า destructors ทั้งหมดเสมือนโดยค่าเริ่มต้น? ค่าใช้จ่ายในการจัดเก็บพิเศษและการเรียกใช้ตารางวิธีเสมือน C ++ ใช้สำหรับระบบความหน่วงต่ำการเขียนโปรแกรม rt ซึ่งอาจเป็นภาระ


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

9
@MarcoA ตั้งแต่เมื่อ destructors บ่งบอกถึงการจัดสรรหน่วยความจำแบบไดนามิก?
chbaker0

@ chbaker0 ฉันใช้ 'like' พวกเขาไม่ได้ใช้ในประสบการณ์ของฉัน
Marco A.

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

@ msalters ที่ทำให้ฉันคิดว่า: ลองนึกภาพโปรแกรมที่ค่าใช้จ่ายในการดำเนินการแต่ละครั้งถูกเก็บไว้ในระบบประเภท การอนุญาตให้ตรวจสอบเวลาคอมไพล์ของการรับประกันแบบเรียลไทม์
Yakk

5

นี่เป็นตัวอย่างที่ดีว่าเมื่อใดที่จะไม่ใช้ destructor เสมือนจริง: จาก Scott Meyers:

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

// class for representing 2D points
class Point {
public:
    Point(short int xCoord, short int yCoord);
    ~Point();
private:
    short int x, y;
};

ถ้า int สั้น ๆ ใช้ 16 บิตวัตถุ Point สามารถใส่ลงทะเบียน 32- บิตได้ นอกจากนี้ยังสามารถส่งวัตถุ Point เป็นปริมาณ 32 บิตไปยังฟังก์ชันที่เขียนในภาษาอื่นเช่น C หรือ FORTRAN อย่างไรก็ตามถ้า destructor ของ Point นั้นเสมือนจริงสถานการณ์จะเปลี่ยนไป

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


If a class does not contain any virtual functions, that is often an indication that it is not meant to be used as a base class.wut มีใครอีกบ้างที่จำวันเก่า ๆ ที่ดีที่เราได้รับอนุญาตให้ใช้คลาสและการสืบทอดเพื่อสร้างชั้นและสมาชิกที่นำกลับมาใช้ซ้ำได้โดยไม่ต้องสนใจวิธีการเสมือนจริงเลย? C'mon สกอตต์ ฉันได้รับจุดสำคัญ แต่ "บ่อย" ถึงจริงๆ
underscore_d

3

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

  • หากอินสแตนซ์ของคลาสที่ได้รับไม่ได้ถูกจัดสรรบนฮีปเช่นบนสแต็กโดยตรงหรือภายในวัตถุอื่น (ยกเว้นถ้าคุณใช้หน่วยความจำที่ไม่ได้กำหนดค่าเริ่มต้นและโอเปอเรเตอร์ตำแหน่งใหม่)
  • หากอินสแตนซ์ของคลาสที่ได้รับถูกจัดสรรบนฮีป แต่การลบจะเกิดขึ้นผ่านพอยน์เตอร์ไปยังคลาสที่ได้รับมากที่สุดเท่านั้นเช่นมี a std::unique_ptr<Derived>และพหุสัณฐานจะเกิดขึ้นผ่านพอยน์เตอร์และการอ้างอิงที่ไม่ใช่เจ้าของเท่านั้น std::make_shared<Derived>()อีกตัวอย่างหนึ่งคือเมื่อวัตถุมีการจัดสรรการใช้ มันเป็นเรื่องปกติที่จะใช้เช่นกันตราบใดที่ชี้เป็นครั้งแรกstd::shared_ptr<Base> std::shared_ptr<Derived>นี่เป็นเพราะพอยน์เตอร์ที่ใช้ร่วมกันมีการแจกจ่ายแบบไดนามิกสำหรับ destructors (deleter) ที่ไม่จำเป็นต้องพึ่งพาตัวทำลายคลาสฐานเสมือน

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

จากนั้นอีกครั้งมีคลาสที่ไม่ได้ออกแบบให้เป็นคลาสพื้นฐาน (สาธารณะ) คำแนะนำส่วนบุคคลของฉันคือการทำให้พวกเขาfinalใน C ++ 11 หรือสูงกว่า หากมันถูกออกแบบให้เป็นหมุดสี่เหลี่ยมก็มีโอกาสที่มันจะไม่ทำงานได้ดีเหมือนหมุดกลม สิ่งนี้เกี่ยวข้องกับการตั้งค่าของฉันสำหรับการมีสัญญาการสืบทอดที่ชัดเจนระหว่างคลาสฐานและคลาสที่ได้รับสำหรับรูปแบบการออกแบบ NVI (ไม่ใช่อินเตอร์เฟสเสมือน) สำหรับนามธรรมแทนที่จะเป็นคลาสฐานคอนกรีตและความเกลียดชังของตัวแปรสมาชิกที่ได้รับการคุ้มครอง แต่ฉันรู้ว่ามุมมองเหล่านี้ขัดแย้งกับระดับใดบ้าง


1

การประกาศ destructor virtualเป็นสิ่งจำเป็นเฉพาะเมื่อคุณวางแผนที่จะทำให้สิ่งที่classสืบทอดได้ โดยปกติแล้วคลาสของไลบรารีมาตรฐาน (เช่นstd::string) จะไม่ให้ตัวทำลายเสมือนและดังนั้นจึงไม่ได้มีไว้สำหรับการทำคลาสย่อย


3
เหตุผลคือ subclassing + การใช้ polymorphism destructor เสมือนเป็นสิ่งจำเป็นเฉพาะในกรณีที่ต้องการความละเอียดแบบไดนามิกนั่นคือการอ้างอิง / ตัวชี้ / สิ่งที่ชั้นต้นแบบจริงอาจหมายถึงอินสแตนซ์ของคลาสย่อย
Michel Billaud

2
@MichelBillaud จริง ๆ แล้วคุณยังสามารถมีความแตกต่างได้โดยไม่ต้องมี dtors เสมือน จำเป็นต้องใช้ dtor เสมือนจริงสำหรับการลบ polymorphic เช่นการเรียกdeleteตัวชี้ไปยังคลาสพื้นฐาน
chbaker0

1

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

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

คุณควรมี destructor เสมือนจริงถ้าคลาสของคุณมีวัตถุประสงค์เป็นคลาสพื้นฐานโดยเฉพาะอย่างยิ่งถ้ามันอาจถูกสร้าง / ทำลายโดยเอนทิตี้อื่น ๆ นอกเหนือจากรหัสที่รู้ว่ามันเป็นประเภทที่สร้างจากนั้นคุณต้อง destructor เสมือน

หากคุณไม่แน่ใจให้ใช้ virtual destructor มันง่ายกว่าที่จะเอาเวอร์ชวลออกหากมันปรากฏขึ้นเป็นปัญหามากกว่าการลองค้นหาบั๊กที่เกิดจาก "ไม่มีการเรียกตัวทำลายที่ถูกต้อง"

ในระยะสั้นคุณไม่ควรมี destructor เสมือนถ้า: 1. คุณไม่มีฟังก์ชั่นเสมือน 2. อย่าได้รับมาจากคลาส (ทำเครื่องหมายfinalใน C ++ 11, วิธีที่คอมไพเลอร์จะบอกถ้าคุณพยายามหามาจากคลาสนั้น)

ในกรณีส่วนใหญ่การสร้างและการทำลายไม่ใช่ส่วนสำคัญของเวลาที่ใช้วัตถุเฉพาะเว้นแต่มี "เนื้อหาจำนวนมาก" (การสร้างสตริง 1MB เห็นได้ชัดว่าต้องใช้เวลาสักระยะเพราะต้องมีข้อมูลอย่างน้อย 1MB ถูกคัดลอกจากที่ใดก็ตามที่อยู่ในปัจจุบัน) การทำลายสตริง 1MB นั้นไม่ได้เลวร้ายไปกว่าการทำลายสตริง 150B ทั้งคู่จะต้องทำการจัดสรรคืนหน่วยเก็บสตริงและไม่มากดังนั้นเวลาที่ใช้โดยทั่วไปจะมีค่าเดียวกัน "รูปแบบพิษ" - แต่นั่นไม่ใช่วิธีที่คุณจะใช้งานแอพพลิเคชั่นจริงของคุณในการผลิต]

ในระยะสั้นมีค่าใช้จ่ายเล็ก ๆ แต่สำหรับวัตถุขนาดเล็กมันอาจสร้างความแตกต่าง

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

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


1
"จะเป็นโอเวอร์เฮดในตัวสร้างสำหรับการสร้าง vtable" - vtable มักจะ "สร้าง" บนพื้นฐานต่อคลาสโดยคอมไพเลอร์กับคอนสตรัคที่มีค่าใช้จ่ายในการจัดเก็บตัวชี้ไปยังวัตถุในอินสแตนซ์การก่อสร้าง
โทนี่

นอกจาก ... ฉันทั้งหมดที่เกี่ยวกับการหลีกเลี่ยงการเพิ่มประสิทธิภาพก่อนวัยอันควร แต่ตรงกันข้ามYou **should** have a virtual destructor if your class is intended as a base-classเป็นเปลือกขั้นต้น - และก่อนวัยอันควรpessimisation สิ่งนี้จำเป็นเฉพาะเมื่อใครก็ตามที่ได้รับอนุญาตให้ลบคลาสที่ได้รับผ่านตัวชี้ไปยังฐาน ในหลาย ๆ สถานการณ์ที่ไม่เป็นเช่นนั้น ถ้าคุณรู้ว่ามันเป็นแล้วตรวจสอบว่าต้องเสียค่าใช้จ่าย ซึ่ง btw จะถูกเพิ่มเสมอแม้ว่าการเรียกที่แท้จริงสามารถแก้ไขได้โดยคอมไพเลอร์ มิฉะนั้นเมื่อคุณควบคุมสิ่งที่ผู้คนสามารถทำกับวัตถุของคุณได้อย่างเหมาะสมมันก็ไม่คุ้มค่า
underscore_d
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.