Liebe Leserinnen, liebe Leser,

heute werde ich über Desktruktoren diskutieren. Warum sind sie einen Beitrag wert, wenn sie slebst nur in besonderen Fällen geschrieben werden, und der Compiler bietet es dennoch an? Die Antwort ist: genau wegen dieses Falles. Weil der Compiler einen Destruktor immer erzeugt, denkt man daran nur in den Fällen, falls eine besondere Arbeitspeicher- oder Statusverwaltung nötig ist. Solche Fällen sind z.B. die Entfernung von durch new-Operator erstellten Objekten mithilfe des delete-Operators (RAII), oder die Rücksetzung von einigen Hardwareteilen zu einem sicheren Zustand. Leider gibt es Situationen, wenn man nur denkt, dass man sich auf den Compiler sicher verlassen kann, den entsprechenden Desktruktor zu erzeugen, aber in der Wirklichkeit kann etwas Anderes auch passieren. Lasst uns den folgenden Codeausschnitt unter die Lupe stellen:

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Furniture {
public:
    // pure virtual methods
    // ...

private:
    // some data members
};

class Table : public Furniture {
public:
    // constructors, operators
    // overridden methods
    
    ~Table();   // special memory management is needed
    
private:
    // more data members
};

// other classes derived from Furniture 


// ...


Table* coffeeTable = new Table();
// do something with coffeeTable
delete coffeeTable;

Furniture* someFurniture = new Table();
// use someFurniture
delete someFurniture;

Aus erster Sicht scheint der Code oben gut zu sein, aber sobald der Lauf des Programms in delete someFurniture ankommt, wird das zu einem undefinierten Verhalten führen. Aber warum? Das Problem mit dem Aufruf von someFurniture ist, dass es den automatisch erzeugten Destruktor von Furniture statt dem Destruktor von Table auslösen wird. Das wurde ursprünglich nicht gewollt. Weil der falsche Desktruktor aufgerufen wurde (Table ist nicht genau ein Furniture, stattdessen ist das nur ein Art von Forniture), wird es ein Problem bei der Arbeitspeicherverwaltung ergeben. Wie kann der Destruktor von Furniture die weiteren Klassenvariablen von Table entfernen, wenn es über die weitere Variablen gar nichts kennt. Dieses undefinierte Verhalten kann durch die Änderung der Funrniture Basisklasse behoben werden:

C++

1
2
3
4
5
6
7
8
9
10
class Furniture {
public:
    // pure virtual methods
    
    virtual ~Furniture() {}
    // ...

private:
    // some data members
};

Ein leerer virtual Desktruktor wurde definiert. Auf diese Weise, wenn man in delete someFurniture ankommt, wird der Name des Destruktors vom Compiler zu einer vtbl-Kennzeichung konvertiert, anstatt einen normalen Funktionsaufruf zu haben. Das Reinergebnis ist der Aufruf des Desktruktors von Table. Dies ist das im meisten Fällen gebrauchte Verhalten, und deswegen könnte man an dem virtual Stichwort denken, dass es den Compiler anweist, den normalen Funktionsaufruf durch eine Kennzeichung zu einem entsprechenden vtbl zu ersetzten, der einen Pointer zu der nötigen Funktion enthält. Das ist dennoch erwähnenswert, dass der Pointer in einem vtbl zurück auf eine Funktion der Basisklasse richten kann, wenn das nicht rein virtual ist, und es keine Override-Funktion in einer abgeleiteten Klasse gibt (z.B.: resizeHeight(double)). Das bedeutet, dass resizeHeight(double) noch eine Kennzeichung zu einem vtbl ist, ungeachtet der Tatsache, dass nur Furniture solche Funktion hat.

Das ist auch erwähnenswert, dass ich darüber nicht ganz sicher bin, ob der Compiler auf die schon erwähnte Weise funktioniert, aber wenn man daran so denkt, ist das einfacher, sich an die angemessene Nutzung von virtual zu erinnern.

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Furniture {
public:
    // pure virtual methods
    virtual void resizeHeight(double d);

    virtual ~Furniture() {}

private:
    // some data members
};

class Table : public Furniture {
public:
    // constructors, operators
    // overridden methods
    
    ~Table();
    
private:
    // more data members
};

// ...

Furniture* coffeeTable = new Table();
coffeeTable->resizeHeight(3.5); // will call resizeHeight inside Furniture;

Weil die Diskussion schon der Klassenstrukturnavigation, die einen eigenen Beitrag wert ist, anzunähern startet, werde ich hier mit der Lektion des Tages aufhören:

Ein virtualer Desktruktor muss in einer Basisklasse immer definiert werden, wenn auch keine Arbeitspeicher- oder Statusverwaltung nötig ist.

Wie immer, vielen Dank fürs Lesen.