GNU GCC (g ++): Mengapa menghasilkan banyak dtor?

90

Lingkungan berkembang: GNU GCC (g ++) 4.1.2

Ketika saya mencoba untuk menyelidiki bagaimana meningkatkan 'cakupan kode - terutama cakupan fungsi' dalam pengujian unit, saya telah menemukan bahwa beberapa dtor kelas tampaknya dibuat beberapa kali. Apakah beberapa dari Anda tahu mengapa?

Saya mencoba dan mengamati apa yang saya sebutkan di atas dengan menggunakan kode berikut.

Dalam "test.h"

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

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

Dalam "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;
}

Ketika saya membangun kode di atas (g ++ test.cpp -o test) dan kemudian melihat jenis simbol apa yang telah dihasilkan sebagai berikut,

nm --demangle test

Saya bisa melihat output berikut.

==== 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()

Pertanyaan saya adalah sebagai berikut.

1) Mengapa beberapa dtor telah dibuat (BaseClass - 2, DerivedClass - 3)?

2) Apa perbedaan di antara para dtor ini? Bagaimana beberapa dtor tersebut akan digunakan secara selektif?

Sekarang saya merasa bahwa untuk mencapai cakupan fungsi 100% untuk proyek C ++, kami perlu memahami ini sehingga saya dapat memanggil semua dtor tersebut dalam pengujian unit saya.

Saya akan sangat menghargai jika seseorang bisa memberi saya jawaban di atas.

Smg
sumber
5
1 untuk menyertakan program sampel minimal dan lengkap. ( sscce.org )
Robᵩ
2
Apakah kelas dasar Anda sengaja memiliki destruktor non-virtual?
Kerrek SB
2
Pengamatan kecil; Anda telah berdosa, dan tidak menjadikan penghancur BaseClass Anda virtual.
Lyke
Maaf untuk sampel saya yang tidak lengkap. Ya, BaseClass harus memiliki penghancur virtual sehingga objek kelas ini dapat digunakan secara polimorfik.
Smg
1
@ Lyke: baik, jika Anda tahu bahwa Anda tidak akan menghapus turunan melalui pointer-to-base tidak apa-apa, saya hanya memastikan ... lucunya, jika Anda membuat anggota basis virtual, Anda mendapatkan balasan lebih banyak perusak.
Kerrek SB

Jawaban:

74

Pertama, tujuan dari fungsi ini dijelaskan dalam Itanium C ++ ABI ; lihat definisi di bawah "base object destructor", "complete object destructor", dan "deleting destructor". Pemetaan ke nama-nama yang rusak diberikan dalam 5.1.4.

Pada dasarnya:

  • D2 adalah "penghancur objek dasar". Ini menghancurkan objek itu sendiri, serta anggota data dan kelas basis non-virtual.
  • D1 adalah "penghancur objek lengkap". Ini juga menghancurkan kelas basis virtual.
  • D0 adalah "menghapus penghancur objek". Itu melakukan semua yang dilakukan destruktor objek lengkap, ditambah panggilan operator deleteuntuk benar-benar membebaskan memori.

Jika Anda tidak memiliki kelas basis virtual, D2 dan D1 identik; GCC akan, pada tingkat pengoptimalan yang memadai, sebenarnya membuat alias simbol ke kode yang sama untuk keduanya.

bdonlan.dll
sumber
Terima kasih atas jawaban yang jelas. Sekarang saya bisa memahami, meskipun saya perlu belajar lebih banyak karena saya tidak begitu akrab dengan jenis warisan virtual.
Smg
@Smg: dalam warisan virtual, kelas yang diwariskan "secara virtual" berada di bawah tanggung jawab tunggal dari objek yang paling diturunkan. Yaitu, jika Anda memiliki struct B: virtual Adan kemudian struct C: B, kemudian saat menghancurkan BAnda memanggil B::D1yang pada gilirannya memanggil A::D2dan saat menghancurkan CAnda memanggil C::D1yang memanggil B::D2dan A::D2(perhatikan bagaimana B::D2tidak memanggil A destruktor). Apa yang benar-benar menakjubkan dalam subdivisi ini adalah dapat mengelola semua situasi dengan hierarki linier sederhana dari 3 penghancur.
Matthieu M.
Hmm, saya mungkin tidak memahami maksudnya dengan jelas ... Saya pikir dalam kasus pertama (merusak objek B), A :: D1 akan dipanggil alih-alih A :: D2. Dan juga dalam kasus kedua (merusak objek C), A :: D1 akan dipanggil alih-alih A :: D2. Apakah aku salah?
Smg
A :: D1 tidak dipanggil karena A bukan kelas tingkat atas di sini; tanggung jawab untuk menghancurkan kelas dasar virtual dari A (yang mungkin ada atau tidak) bukan milik A, melainkan milik D1 atau D0 dari kelas tingkat atas.
bdonlan
37

Biasanya ada dua varian konstruktor ( not-in-charge / in-charge ) dan tiga destruktor ( not-in-charge / in-charge / in-charge menghapus ).

The tidak-in-charge ctor dan dtor digunakan ketika menangani sebuah objek dari sebuah kelas yang mewarisi dari kelas lain menggunakan virtualkata kunci, ketika objek bukan objek lengkap (sehingga objek saat ini "tidak bertanggung jawab" membangun atau Destructing objek basis virtual). Ctor ini menerima pointer ke objek basis virtual dan menyimpannya.

The di-charge ctor dan dtors adalah untuk semua kasus lain, yaitu jika tidak ada virtual warisan yang terlibat; jika kelas memiliki penghancur virtual, penunjuk dtor penghapusan yang bertanggung jawab masuk ke slot vtable, sementara cakupan yang mengetahui tipe dinamis dari objek (yaitu untuk objek dengan durasi penyimpanan otomatis atau statis) akan menggunakan dtor yang bertanggung jawab (karena memori ini tidak boleh dibebaskan).

Contoh kode:

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);

Hasil:

  • Entri dtor di masing-masing tabel untuk foo, bazdan quuxmenunjuk ke dtor yang bertanggung jawab menghapus masing-masing .
  • b1dan b2dibangun oleh baz() in-charge , yang memanggil foo(1) -in-charge
  • q1dan q2dibangun oleh quux() in-charge , yang foo(2) bertanggung jawab dan baz() tidak-in-charge dengan penunjuk ke fooobjek yang dibangun sebelumnya
  • q2dihancurkan oleh ~auto_ptr() in-charge , yang memanggil dtor virtual yang ~quux() bertanggung jawab menghapus , yang memanggil ~baz() not-in-charge , ~foo() in-charge dan operator delete.
  • q1dihancurkan oleh ~quux() in-charge , yang menyebut ~baz() not-in-charge dan ~foo() in-charge
  • b2dihancurkan oleh ~auto_ptr() in-charge , yang memanggil dtor virtual yang ~baz() bertanggung jawab menghapus , yang memanggil ~foo() in-charge danoperator delete
  • b1dihancurkan oleh ~baz() in-charge , yang memanggil ~foo() in-charge

Siapa pun yang berasal dari quuxakan menggunakan yang tidak-in-charge ctor dan dtor dan mengambil tanggung jawab menciptakan fooobjek.

Pada prinsipnya, varian not-in-charge tidak pernah diperlukan untuk kelas yang tidak memiliki basis virtual; dalam hal ini, varian in-charge kadang-kadang disebut bersatu , dan / atau simbol untuk in-charge dan tidak-in-charge disebut juga sebagai implementasi tunggal.

Simon Richter
sumber
Terima kasih atas penjelasan Anda yang jelas sehubungan dengan contoh yang cukup mudah dipahami. Dalam hal warisan virtual terlibat, itu adalah tanggung jawab kelas yang paling diturunkan untuk membuat objek kelas basis virtual. Adapun kelas-kelas lain selain kelas yang paling banyak diturunkan, mereka seharusnya ditafsirkan oleh konstruktor tidak-in-charge sehingga mereka tidak menyentuh kelas basis virtual.
Smg
Terima kasih atas penjelasan yang sangat jelas. Saya ingin mendapatkan klarifikasi tentang lebih banyak hal, bagaimana jika kita tidak menggunakan auto_ptr dan sebaliknya mengalokasikan memori di konstruktor dan menghapus di destruktor. Dalam hal ini, apakah kita hanya memiliki dua destruktor yang tidak bertanggung jawab / sedang menghapus?
nonenone
1
@bhavin, tidak, penyiapannya tetap sama persis. Kode yang dihasilkan untuk destruktor selalu menghancurkan objek itu sendiri dan sub-objek apa pun, jadi Anda mendapatkan kode untuk deleteekspresi baik sebagai bagian dari destruktor Anda sendiri, atau sebagai bagian dari panggilan destruktor sub-objek. The deleteekspresi diimplementasikan baik sebagai panggilan melalui vtable objek jika memiliki destructor virtual (di mana kita menemukan di-charge menghapus , atau sebagai panggilan langsung ke objek yang di-charge destructor.
Simon Richter
Sebuah deleteekspresi tidak pernah menyebut tidak-in-charge varian, yang hanya digunakan oleh destructors lain ketika menghancurkan sebuah benda yang menggunakan virtual warisan.
Simon Richter