Liebe Leserinnen, liebe Leser,

heute werde ich über eine der exotischen (aber nicht ungewöhnlichen) Mehrfachvererbungen diskutieren: die Diamantenvererbung. Zuerst ein bisschen Code:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>
#include <unordered_set>

class Boat {
public:
    virtual ~Boat() {}
    
    virtual void dropAnchor() const {
        std::cout << "Anchor dropped." << std::endl;
    }
    virtual void raiseAnchor() const {
        std::cout << "Anchor raised." << std::endl;
    }
};

class SailingBoat : public Boat {
public:
    virtual void lowerSails() const {
        std::cout << "Sails lowered." << std::endl;
    }
    
    virtual void hoistSails() const {
        std::cout << "Sails raised" << std::endl;
    }
};

class MotorBoat : public Boat {
public:
    virtual void startEngine() const {
        std::cout << "Engine started" << std::endl;
    }
    
    virtual void stopEngine() const {
        std::cout << "Engine stopped" << std::endl;
    }
};

class MotorSailingYacht : public SailingBoat, public MotorBoat {
public:
    // ...
};

class Port {
public:
    void moorBoat(Boat* b) {
        mooredBoats.insert(b);
        std::cout << "Boat moored." << std::endl;
    }
        
    void fuelBoatAndCheckRig(MotorSailingYacht* msy) {
        // look up msy from mooredBoats and do stuff
        std::cout << "Boat fueled and rig checked." << std::endl;
    }
    
private:
    std::unordered_set<Boat*> mooredBoats;
};

int main()
{
    Port port;
    MotorSailingYacht msYacht;
    
    port.moorBoat(&msYacht);
}

name

Sowohl der Code, als auch das Klassendiagramm beweist, woraus der Name dieser spezifischen Art von Mehrfachvererbung stammt, deshalb werde ich darüber nicht mehr sagen. Die interessante Sache erstellt sich, wenn man den Code zu kompilieren versucht. Es wird scheitern natürlich, wie es der Leser/die Leserin wahrscheinlich schon vermutet hat. Hier ist die Fehlermeldung:

Make

1
2
3
4
test.cpp: In function ‘int main()’:
test.cpp:64:27: error: ‘Boat’ is an ambiguous base of ‘MotorSailingYacht’
     port.moorBoat(&msYacht);
                           ^

Der Name dieses Problems ist das Diamantenproblem, das erbt diesen Namen aus dieser Art von Vererbung. Also, warum passiert das? Einfach gesagt stammt es aus der Sprache (und aus die Weise, wie sie funktioniert): Weil MotorSailingYacht die Basisklasse gleichzeitig durch SailingBoat und MotorBoat erbt, würde MotorSailingYacht normalerweise zwei verschiedene Boat Unterobjekte haben. Das würde natürlich zu einer Ambiguität führen, wenn das Yacht-Objekt zu der moorBoat() Funktion weitergegeben wird (weitere Informationen über diese Sache sind am Ende dieses Beitrags durch den Link zu finden). Diese Ambiguität ist offensichtlicher, wenn man eine Basisklassenfunktion aufzurufen versucht, wie z.B.: dropAnchor():

C++

1
2
3
4
5
6
7
8
9
// ...

int main()
{
    Port port;
    MotorSailingYacht msYacht;
  
    msYacht.dropAnchor();
}

Die dazugehörige Fehlermeldung drückt das besser aus: obwohl es zwei Alterntiven gibt, ist es unmöglich zu bestimmen, welche ausgewählt werden soll:

Make

1
2
3
4
5
6
7
test.cpp:64:13: error: request for member ‘dropAnchor’ is ambiguous
     msYacht.dropAnchor();
             ^~~~~~~~~~
test.cpp:8:18: note: candidates are: virtual void Boat::dropAnchor() const
     virtual void dropAnchor() const {
                  ^~~~~~~~~~
test.cpp:8:18: note:                 virtual void Boat::dropAnchor() const

Als es sich herausstellte, gibt es mehrere Lösungen für dieses Problem. Die erste Lösung ist explizite Qualifikation, und die andere ist static_cast. Hier gibt’s zwei Beispiele, einschießlich Versionen mit Zeigern:

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...

int main()
{
    Port port;
    MotorSailingYacht msYacht;
    MotorSailingYacht* msYachtPtr = &msYacht;
  
    msYacht.MotorBoat::dropAnchor();
    msYachtPtr->MotorBoat::raiseAnchor();
    
    static_cast<MotorBoat&>(msYacht).dropAnchor();
    static_cast<MotorBoat*>(msYachtPtr)->raiseAnchor();
    
    port.moorBoat(static_cast<MotorBoat*>(&msYacht));
}

Dieser Code kann kompiliert und ausgeführt werden, aber bevor ich mehr sagen werde, möchte is das auch deutlich machen, dass trotzdem ich immer MotorBoat für die Lösung der Ambiguität verwendet habe, könnte SailingBoat auch verwendet werden, um die gleiche Ergebnis zu bekommen. Ist das alles? Leider noch nicht, weil diese Lösung inhärente Probleme hat. Was würde passieren, wenn man einen Boat Zeiger für die Yacht haben will, um die mehrfache Konvertierung bei Boat* Zeigern zu vermeiden, aber mittlerweile gibt es auch eine andere Funktion, die einen MotorSailingYacht Zeiger braucht? Eine Rückwärtskonvertierung ist natürlich die Lösung…

C++

1
2
3
4
5
6
7
8
9
10
11
int main()
{
    Port port;
    MotorSailingYacht msYacht;
    Boat* boatPtr = static_cast<MotorBoat*>(&msYacht);
    port.moorBoat(boatPtr);
    
    // ...
    
    port.fuelBoatAndCheckRig(static_cast<MotorSailingYacht*>(boatPtr));
}

…aber das wird nicht funktionieren, weil Boat während der Zurückkonvertierung immer noch doppeldeutig ist. Man könnte natürlich den Boat Zeiger in MotorBoat oder in SailingBoat Zeiger konvertieren, und dann diesen neuen Zeiger (mit impliziter Konvertierung) benutzten, aber das ist ganz mühsam, einen Zeiger für alle Anwendungfälle zu haben. Das ist nicht eine ideale Lösung, besonders wenn man bedenkt, dass alle diese statische Typkonvertierungen und verschiedene Zeiger sehr verwirrend sein können. Glücklicheweise gibt es eine dritte Weise, auf die das Diamantenproblem einfach gelöst werden kann, namens virtuelle Vererbung. Nachdem die MotorBoat und SailingBoat Klassen ein bisschen geändert wurden (das ist bemerkenswert, dass die MotorSailingYacht Klasse keine Modifikation braucht), alles startet zu funktionieren:

C++

1
2
3
4
5
6
7
8
9
class SailingBoat : virtual public Boat {
public:
    // ...
};

class MotorBoat : virtual public Boat {
public:
    // ...
};

Wegen der Einfügung des virtual Spezifikationssymbols ist es deklariert, dass MotorSailingYacht nur eine gemeinsame Boat Instanz hat, oder anders ausgedrückt, sie ist zwischen MotorSailingYacht::MotorBoat und MotorSailingYacht::SailingBoat geteilt, und deswegen wird der ursprüngliche Code in main() (mit alle möglichen Funktionsaufrufen) wie erwartet funktionieren. Man könnte jetzt die Frage stellen, ob virtuelle Vererbung Nachteile hat? Der Nachteil ist, dass die Größe des Objekts wegen des Bedarfs für zusäztliche vtables und vptrs (genauso wie bei Mehrfachvererbung) erhöhen wird. Neue Probleme werden auch eingeführt, weil Boat statt MotorBoat oder SailingBoat ein Teil der MotorSailingYacht Klasse ist (das bedeutet, dass statische Klassenkonvertierung nicht mehr nutzbar ist, sondern eine teurere dynamische Konvertierung ist gefordert). Eine detailliertere Diskussion dieses Themas würde dennoch den Rahmen dieses Beitrags sprengen, also befragen Sie bitte den folgenden ausgezeichneten Beitrag auf Dr. Dobb’s, den ich für etwas völling Empfehlenswertes halte. Die Lektion des Tages ist:

Diamantenvererbung kann verwendet werden, wenn es nötig ist, aber das ist auf keinen Fall ein Allheilmittel.

Wie immer, vielen Dank fürs Lesen.