GNU GCC (g ++): เหตุใดจึงสร้าง dtors หลายตัว


91

การพัฒนาสภาพแวดล้อม: GNU GCC (g ++) 4.1.2

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

ฉันพยายามและสังเกตสิ่งที่ฉันกล่าวถึงข้างต้นโดยใช้รหัสต่อไปนี้

ใน "test.h"

class BaseClass
{
public:
    ~BaseClass();
    void someMethod();
};

class DerivedClass : public BaseClass
{
public:
    virtual ~DerivedClass();
    virtual void someMethod();
};

ใน "test.cpp"

#include <iostream>
#include "test.h"

BaseClass::~BaseClass()
{
    std::cout << "BaseClass dtor invoked" << std::endl;
}

void BaseClass::someMethod()
{
    std::cout << "Base class method" << std::endl;
}

DerivedClass::~DerivedClass()
{
    std::cout << "DerivedClass dtor invoked" << std::endl;
}

void DerivedClass::someMethod()
{
    std::cout << "Derived class method" << std::endl;
}

int main()
{
    BaseClass* b_ptr = new BaseClass;
    b_ptr->someMethod();
    delete b_ptr;
}

เมื่อฉันสร้างโค้ดด้านบน (g ++ test.cpp -o test) จากนั้นดูว่ามีการสร้างสัญลักษณ์ประเภทใดดังนี้

นาโนเมตร - การทดสอบความผิดเพี้ยน

ฉันเห็นผลลัพธ์ต่อไปนี้

==== following is partial output ====
08048816 T DerivedClass::someMethod()
08048922 T DerivedClass::~DerivedClass()
080489aa T DerivedClass::~DerivedClass()
08048a32 T DerivedClass::~DerivedClass()
08048842 T BaseClass::someMethod()
0804886e T BaseClass::~BaseClass()
080488f6 T BaseClass::~BaseClass()

คำถามของฉันมีดังนี้

1) เหตุใดจึงสร้าง dtors หลายตัว (BaseClass - 2, DerivedClass - 3)

2) อะไรคือความแตกต่างระหว่าง dtors เหล่านี้? วิธีการเลือกใช้ dtors หลายตัวเหล่านี้?

ตอนนี้ฉันรู้สึกว่าเพื่อให้ได้ฟังก์ชันครอบคลุม 100% สำหรับโปรเจ็กต์ C ++ เราจำเป็นต้องเข้าใจสิ่งนี้เพื่อที่ฉันจะสามารถเรียกใช้ dtors เหล่านั้นทั้งหมดในการทดสอบหน่วยของฉันได้

ฉันจะขอบคุณเป็นอย่างยิ่งหากมีใครสามารถตอบกลับฉันด้านบนได้


5
+1 สำหรับการรวมโปรแกรมตัวอย่างขั้นต่ำที่สมบูรณ์ ( sscce.org )
Robᵩ

2
คลาสพื้นฐานของคุณมีเจตนาทำลายล้างที่ไม่ใช่เสมือนจริงหรือไม่?
Kerrek SB

2
ข้อสังเกตเล็กน้อย คุณได้ทำบาปและไม่ได้ทำให้ BaseClass destructor ของคุณเป็นเสมือน
Lyke

ขออภัยสำหรับตัวอย่างที่ไม่สมบูรณ์ของฉัน ใช่ BaseClass ควรมีตัวทำลายเสมือนเพื่อให้สามารถใช้อ็อบเจ็กต์คลาสเหล่านี้ได้หลายแบบ
Smg

1
@ ไลค์: ถ้าคุณรู้ว่าคุณจะไม่ลบไฟล์ที่ได้มาจากตัวชี้ไปยังฐานก็ไม่เป็นไรฉันแค่ทำให้แน่ใจว่า ... สนุกดีถ้าคุณทำให้สมาชิกฐานเสมือนคุณจะได้ ผู้ทำลายมากขึ้น
Kerrek SB

คำตอบ:


75

ครั้งแรกวัตถุประสงค์ของฟังก์ชั่นเหล่านี้จะอธิบายไว้ในItanium c ++ ABI ; ดูคำจำกัดความภายใต้ "ตัวทำลายวัตถุพื้นฐาน" "ตัวทำลายวัตถุที่สมบูรณ์" และ "การลบตัวทำลาย" การแมปกับชื่อที่แหลกเหลวมีให้ใน 5.1.4

โดยทั่วไป:

  • D2 คือ "ตัวทำลายวัตถุพื้นฐาน" มันทำลายออบเจ็กต์เองเช่นเดียวกับสมาชิกข้อมูลและคลาสพื้นฐานที่ไม่ใช่เสมือน
  • D1 คือ "ตัวทำลายออบเจ็กต์ที่สมบูรณ์" นอกจากนี้ยังทำลายคลาสพื้นฐานเสมือน
  • D0 คือ "การลบตัวทำลายวัตถุ" มันทำทุกอย่างที่ตัวทำลายออบเจ็กต์ที่สมบูรณ์ทำแถมยังเรียกร้องoperator deleteให้ปลดปล่อยหน่วยความจำอีกด้วย

หากคุณไม่มีคลาสพื้นฐานเสมือน D2 และ D1 จะเหมือนกัน GCC จะในระดับการเพิ่มประสิทธิภาพที่เพียงพอจริง ๆ แล้วจะใช้สัญลักษณ์แทนรหัสเดียวกันสำหรับทั้งสองอย่าง


ขอบคุณสำหรับคำตอบที่ชัดเจน ตอนนี้ฉันสามารถเชื่อมโยงได้แล้วแม้ว่าฉันจะต้องศึกษาเพิ่มเติมเนื่องจากฉันไม่ค่อยคุ้นเคยกับสิ่งของประเภทการสืบทอดเสมือน
Smg

@Smg: ในการสืบทอดเสมือนคลาสที่สืบทอด "แทบ" อยู่ภายใต้ความรับผิดชอบของวัตถุที่ได้รับมากที่สุด แต่เพียงผู้เดียว นั่นคือถ้าคุณมีstruct B: virtual Aแล้วstruct C: Bแล้วเมื่อทำลายBคุณเรียกB::D1ซึ่งในรอบวิงวอนA::D2และเมื่อทำลายCคุณเรียกC::D1ที่เรียกB::D2และA::D2(หมายเหตุว่าB::D2ไม่เรียก destructor) สิ่งที่น่าทึ่งมากในแผนกนี้คือสามารถจัดการสถานการณ์ทั้งหมดได้จริงด้วยลำดับชั้นเชิงเส้นที่เรียบง่ายของตัวทำลาย3ตัว
Matthieu M.

อืมฉันอาจจะไม่เข้าใจประเด็นนี้อย่างชัดเจน ... ฉันคิดว่าในกรณีแรก (การทำลายวัตถุ B) A :: D1 จะถูกเรียกแทน A :: D2 และในกรณีที่สอง (ทำลายวัตถุ C) A :: D1 จะถูกเรียกแทน A :: D2 ฉันผิดเหรอ?
Smg

A :: D1 ไม่ถูกเรียกใช้เนื่องจาก A ไม่ใช่คลาสระดับบนสุดที่นี่ ความรับผิดชอบในการทำลายคลาสพื้นฐานเสมือนของ A (ซึ่งอาจมีหรือไม่มีอยู่) ไม่ได้เป็นของ A แต่เป็นของ D1 หรือ D0 ของคลาสระดับบนสุด
bdonlan

37

โดยปกติจะมีคอนสตรัคเตอร์สองแบบ ( ไม่คิดค่าใช้จ่าย / ในการชาร์จ ) และตัวทำลายสามตัว (การลบแบบไม่คิดค่าใช้จ่าย / ในค่าใช้จ่าย / ในการชาร์จ )

ไม่เสียค่าใช้จ่าย ctor และ dtor จะใช้เมื่อจัดการวัตถุของคลาสที่สืบทอดจากคลาสอื่นโดยใช้ที่virtualคำหลักเมื่อวัตถุไม่ได้เป็นวัตถุที่สมบูรณ์ (ดังนั้นวัตถุปัจจุบันคือ "ไม่ได้อยู่ในค่าใช้จ่าย" ในการสร้างหรือ destructing วัตถุฐานเสมือน) ctor นี้รับตัวชี้ไปยังวัตถุฐานเสมือนและเก็บไว้

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

ตัวอย่างโค้ด:

struct foo {
    foo(int);
    virtual ~foo(void);
    int bar;
};

struct baz : virtual foo {
    baz(void);
    virtual ~baz(void);
};

struct quux : baz {
    quux(void);
    virtual ~quux(void);
};

foo::foo(int i) { bar = i; }
foo::~foo(void) { return; }

baz::baz(void) : foo(1) { return; }
baz::~baz(void) { return; }

quux::quux(void) : foo(2), baz() { return; }
quux::~quux(void) { return; }

baz b1;
std::auto_ptr<foo> b2(new baz);
quux q1;
std::auto_ptr<foo> q2(new quux);

ผล:

  • รายการ dtor ในแต่ละ vtables สำหรับfoo, bazและquuxจุดที่เกี่ยวข้องในค่าใช้จ่ายการลบ dtor
  • b1และb2สร้างขึ้นโดยbaz() การเรียกเก็บเงินซึ่งเรียกfoo(1) เก็บเงิน
  • q1และq2สร้างขึ้นโดยquux() ประจุไฟฟ้าซึ่งตกอยู่foo(2) ในประจุและbaz() ไม่อยู่ในประจุโดยมีตัวชี้ไปยังfooวัตถุที่สร้างขึ้นก่อนหน้านี้
  • q2เป็น destructed โดย~auto_ptr() เสียค่าใช้จ่ายซึ่งเรียก dtor เสมือน~quux() การลบในค่าใช้จ่ายที่เรียก~baz() ไม่เสียค่าใช้จ่าย , ~foo() ในค่าใช้จ่ายoperator deleteและ
  • q1ถูกทำลายโดย~quux() การเรียกเก็บเงินซึ่งเรียกว่า~baz() ไม่เรียกเก็บเงินและ~foo() ไม่มีค่าใช้จ่าย
  • b2ถูกทำลายโดย~auto_ptr() การเรียกเก็บเงินซึ่งเรียกการ~baz() ลบค่าใช้จ่าย dtor เสมือนซึ่งเรียก~foo() ค่าใช้จ่ายและoperator delete
  • b1ถูกทำลายโดย~baz() ผู้รับผิดชอบซึ่งเรียก~foo() เก็บเงิน

ใครก็ตามที่ได้มาจากquuxจะใช้ctor และ dtor ที่ไม่คิดค่าบริการและรับผิดชอบในการสร้างfooวัตถุ

โดยหลักการแล้วตัวแปรที่ไม่คิดค่าใช้จ่ายไม่จำเป็นสำหรับคลาสที่ไม่มีฐานเสมือน ในกรณีนั้นตัวแปรที่มีค่าใช้จ่ายบางครั้งเรียกว่าunifiedและ / หรือสัญลักษณ์สำหรับทั้งที่คิดค่าใช้จ่ายและไม่อยู่ในค่าใช้จ่ายจะถูกใช้นามแฝงเป็นการนำไปใช้งานเดียว


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

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

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

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