GNU GCC (g ++): Warum werden mehrere Dtoren generiert?

89

Entwicklungsumgebung: GNU GCC (g ++) 4.1.2

Während ich versuche zu untersuchen, wie die Codeabdeckung - insbesondere die Funktionsabdeckung - beim Komponententest erhöht werden kann, habe ich festgestellt, dass ein Teil der Klasse dtor anscheinend mehrmals generiert wird. Haben einige von Ihnen eine Idee, warum, bitte?

Ich habe versucht und beobachtet, was ich oben erwähnt habe, indem ich den folgenden Code verwendet habe.

In "test.h"

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

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

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

Wenn ich den obigen Code erstellt habe (g ++ test.cpp -o test) und dann sehe, welche Art von Symbolen wie folgt generiert wurden:

nm - Entwirrungstest

Ich konnte die folgende Ausgabe sehen.

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

Meine Fragen lauten wie folgt.

1) Warum wurden mehrere Dtoren generiert (BaseClass - 2, DerivedClass - 3)?

2) Was ist der Unterschied zwischen diesen Dtoren? Wie werden diese mehreren Dtoren selektiv verwendet?

Ich habe jetzt das Gefühl, dass wir dies verstehen müssen, um eine 100% ige Funktionsabdeckung für C ++ - Projekte zu erreichen, damit ich alle diese Dtoren in meinen Komponententests aufrufen kann.

Ich würde mich sehr freuen, wenn mir jemand die Antwort auf das oben Gesagte geben könnte.

Smg
quelle
5
+1 für die Aufnahme eines minimalen, vollständigen Beispielprogramms. ( sscce.org )
Robᵩ
2
Hat Ihre Basisklasse absichtlich einen nicht virtuellen Destruktor?
Kerrek SB
2
Eine kleine Beobachtung; Sie haben gesündigt und Ihren BaseClass-Destruktor nicht virtuell gemacht.
Lyke
Entschuldigung für meine unvollständige Probe. Ja, die BaseClass sollte über einen virtuellen Destruktor verfügen, damit diese Klassenobjekte polymorph verwendet werden können.
Smg
1
@ Lyke: Nun, wenn Sie wissen, dass Sie ein abgeleitetes Element nicht durch einen Zeiger auf die Basis löschen werden, der in Ordnung ist, habe ich nur dafür gesorgt, dass ... lustig, wenn Sie die Basismitglieder virtuell machen, erhalten Sie sogar mehr Zerstörer.
Kerrek SB

Antworten:

72

Zunächst werden die Zwecke dieser Funktionen im Itanium C ++ ABI beschrieben . Siehe Definitionen unter "Basisobjekt-Destruktor", "Vollständiger Objekt-Destruktor" und "Löschen des Destruktors". Die Zuordnung zu verstümmelten Namen ist in 5.1.4 angegeben.

Grundsätzlich:

  • D2 ist der "Basisobjekt-Destruktor". Es zerstört das Objekt selbst sowie Datenelemente und nicht virtuelle Basisklassen.
  • D1 ist der "vollständige Objektzerstörer". Es zerstört zusätzlich virtuelle Basisklassen.
  • D0 ist der "Objektzerstörer löschen". Es macht alles, was der gesamte Objektzerstörer macht, und es ruft operator deleteauf, um den Speicher tatsächlich freizugeben.

Wenn Sie keine virtuellen Basisklassen haben, sind D2 und D1 identisch. GCC wird bei ausreichenden Optimierungsstufen die Symbole tatsächlich für beide auf denselben Code aliasen.

bdonlan
quelle
Vielen Dank für die klare Antwort. Jetzt, wo ich mich darauf beziehen kann, obwohl ich mehr lernen muss, da ich mit Sachen der virtuellen Vererbung nicht so vertraut bin.
Smg
@Smg: Bei der virtuellen Vererbung unterliegen die "virtuell" geerbten Klassen der alleinigen Verantwortung des am meisten abgeleiteten Objekts. Das heißt, wenn Sie haben struct B: virtual Aund dann , wenn Sie einen struct C: Bzerstören B, rufen Sie auf, B::D1was wiederum aufruft, A::D2und wenn Sie einen zerstören C, rufen Sie auf, C::D1was aufruft B::D2und A::D2(beachten Sie, wie B::D2kein Destruktor aufgerufen wird). Was in dieser Unterteilung wirklich erstaunlich ist, ist, tatsächlich alle Situationen mit einer einfachen linearen Hierarchie von 3 Destruktoren verwalten zu können.
Matthieu M.
Hmm, ich habe den Punkt vielleicht nicht klar verstanden ... Ich dachte, dass im ersten Fall (Zerstörung des B-Objekts) A :: D1 anstelle von A :: D2 aufgerufen wird. Und auch im zweiten Fall (Zerstörung des C-Objekts) wird A :: D1 anstelle von A :: D2 aufgerufen. Liege ich falsch?
Smg
A :: D1 wird nicht aufgerufen, da A hier nicht die Klasse der obersten Ebene ist. Die Verantwortung für die Zerstörung virtueller Basisklassen von A (die möglicherweise vorhanden sind oder nicht) liegt nicht bei A, sondern bei D1 oder D0 der obersten Klasse.
Bdonlan
37

Es gibt normalerweise zwei Varianten des Konstruktors ( nicht verantwortlich / verantwortlich ) und drei des Destruktors ( nicht verantwortlich / verantwortlich / verantwortlich löschen ).

Der nicht verantwortliche ctor und dtor werden verwendet, wenn ein Objekt einer Klasse, die von einer anderen Klasse erbt, mit dem virtualSchlüsselwort behandelt wird, wenn das Objekt nicht das vollständige Objekt ist (das aktuelle Objekt ist also "nicht verantwortlich" für das Konstruieren oder Zerstören das virtuelle Basisobjekt). Dieser ctor empfängt einen Zeiger auf das virtuelle Basisobjekt und speichert es.

Der verantwortliche ctor und dtors gelten für alle anderen Fälle, dh wenn keine virtuelle Vererbung vorliegt; Wenn die Klasse über einen virtuellen Destruktor verfügt, wird der zuständige Lösch-dtor-Zeiger in den vtable-Slot verschoben, während ein Bereich, der den dynamischen Typ des Objekts kennt (dh für Objekte mit automatischer oder statischer Speicherdauer), den verantwortlichen dtor verwendet (weil dieser Speicher nicht freigegeben werden sollte).

Codebeispiel:

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

Ergebnisse:

  • Der dtor Eintrag in jedem des VTables für foo, bazund quuxPunkt , an den jeweiligen in-charge Löschen dtor.
  • b1und b2werden von baz() Verantwortlichen konstruiert , die foo(1) Verantwortliche anrufen
  • q1und q2werden von einem quux() Verantwortlichen konstruiert , der mit einem Zeiger auf das zuvor konstruierte Objekt foo(2) verantwortlich und baz() nicht verantwortlich istfoo
  • q2wird durch den ~auto_ptr() Verantwortlichen zerstört , der das ~quux() Löschen des virtuellen Verantwortlichen aufruft, der das ~baz() Nicht-Verantwortliche , das ~foo() Verantwortliche und das Aufrufen aufruft operator delete.
  • q1wird durch ~quux() Verantwortliche zerstört , die ~baz() Nicht-Verantwortliche und ~foo() Verantwortliche anrufen
  • b2wird zerstört durch ~auto_ptr() in-charge , die die virtuelle dtor ruft ~baz() in-charge Löschen , die Anrufe ~foo() in-Gebühr undoperator delete
  • b1wird durch destructed ~baz() in-charge , die Anrufe ~foo() in-Gebühr

Jeder, der von stammt, quuxwürde seinen nicht verantwortlichen ctor und dtor verwenden und die Verantwortung für die Erstellung des fooObjekts übernehmen.

Im Prinzip wird die nicht verantwortliche Variante niemals für eine Klasse benötigt, die keine virtuellen Basen hat. In diesem Fall wird die verantwortliche Variante manchmal als einheitlich bezeichnet , und / oder die Symbole für sowohl verantwortlich als auch nicht verantwortlich sind auf eine einzelne Implementierung ausgerichtet.

Simon Richter
quelle
Vielen Dank für Ihre klare Erklärung in Verbindung mit einem leicht verständlichen Beispiel. Für den Fall, dass es sich um eine virtuelle Vererbung handelt, liegt es in der Verantwortung der am meisten abgeleiteten Klasse, ein virtuelles Basisklassenobjekt zu erstellen. Die anderen Klassen als die am meisten abgeleitete Klasse sollten von einem nicht verantwortlichen Konstruktor ausgelegt werden, damit sie die virtuelle Basisklasse nicht berühren.
Smg
Vielen Dank für die kristallklare Erklärung. Ich wollte mehr darüber erfahren, was passiert, wenn wir nicht auto_ptr verwenden und stattdessen Speicher im Konstruktor zuweisen und im Destruktor löschen. In diesem Fall hätten wir nur zwei Destruktoren, die nicht verantwortlich sind / die gelöscht werden?
Nonenone
1
@ Bhavin, nein, das Setup bleibt genau gleich. Der generierte Code für einen Destruktor zerstört immer das Objekt selbst und alle Unterobjekte, sodass Sie den Code für den deleteAusdruck entweder als Teil Ihres eigenen Destruktors oder als Teil der Unterobjekt-Destruktoraufrufe erhalten. Der deleteAusdruck wird entweder als Aufruf über die vtable des Objekts implementiert, wenn er über einen virtuellen Destruktor verfügt (wo wir das Löschen von Verantwortlichen finden , oder als direkter Aufruf an den verantwortlichen Destruktor des Objekts)
Simon Richter,
Ein deleteAusdruck ruft niemals die nicht verantwortliche Variante auf, die nur von anderen Destruktoren verwendet wird, während ein Objekt zerstört wird, das virtuelle Vererbung verwendet.
Simon Richter