Deadly Diamond of Death - Teil 2
Das Diamantenproblem von letztem Mal ist leider nicht durch die virtuelle Vererbung einfach so zu lösen. Auch wenn nur noch ein Subobjekt der gemeinsamen Basisklasse in der Objekthierarchie vorhanden ist, kann es durch unterschiedliche Überschreibungen von virtuellen Funktionen zu Problemen kommen. Ein sehr ähnliches Problem entsteht auch dann, wenn es keine gemeinsame Basisklasse gibt, sondern zufällig zwei Funktionen aus unterschiedlichen Basisklassen die gleiche Signatur aufweisen.
Video
Code
Variante des Diamantenproblems
|
|
Kollision aufgrund zufällig gleicher Signatur
|
|
Erklärung
Beide Listings lösen das gleiche Problem aus: Funktionen gleichen Namens und gleicher Signatur kollidieren in der Klasse
Join
, welche sie aus verschiedenen Teilen der Vererbungshierarchie zusammenführt. Die Ursache unterscheidet sich
allerdings leicht: im Falle des Diamantenproblems liegt die Ursache in der gemeinsamen Basisklasse begraben. Dort wird die
Funktion sayHello()
aus der Klasse Base
in den Klassen Left
und Right
überschrieben. Das ist in dem Fall notwendig,
wenn die entsprechenden Klassen auch instanziiert werden sollen. Im zweiten Fall ist die Ursache etwas anders: die
Funktionen in den Basisklassen Left
und Right
heißen nur zufällig gleich und haben die gleiche Signatur. Das kann einem
bei der Verwendung von Mehrfachvererbung immer mal wieder passieren. Man mischt in einer eigenen Klasse zwei Basisklassen
aus unterschiedlichen Bibliotheken und schon passiert eine derartige Kollision.
Die Lösung ist für beide Probleme die gleiche: Die Klasse Join
muss die betreffende Funktion überschreiben und damit
eindeutig festlegen, welche Implementierung zu verwenden ist. Der Compiler muss im Hintergrund leicht unterschiedliche Dinge
tun: im Falle des ersten Listings schreibt er den Eintrag für sayHello()
in die vtable der Klasse Base
, aus der die
Funktion ja ursprünglich kommt. Im zweiten Listing hat er aus den unterschiedlichen Basisklassen zwei vtables, in denen es
jeweils einen Eintrag für sayHello()
gibt. Damit müssen wir uns zum Glück nicht rumschlagen: der Compiler trägt völlig
korrekt in beide vtables und neue Implementierung ein, so dass sich die Objekte auch bei Zugriff über die Referenzen auf die
jeweiligen Basisklassen wie gewohnt verhalten.
Ein Wort noch zu dem zweiten Fall: man sollte immer genau prüfen, ob beide Basisklassen mit den kollidierenden Funktionen
auch wirklich das gleiche Konzept ausdrücken. Ein (etwas konstruiertes Beispiel): wir haben eine Bibliothek für grafische
Bedienelemente und die Standard Template Library von C++. Nun kommen wir auf die Idee, ein Textfeld implementieren zu wollen,
welches man gleichzeitig als std::string
verwenden kann (in dem Fall wird einfach der aktuelle Inhalt des Textfeldes als
String verwendet). Unser naiver Ansatz, einfach von (der gedachten) TextField-Klasse und std::string
parallel abzuleiten
scheitert an length()
. Beide Klassen bieten diese Funktion an: TextField als grafische Länge des Textfeldes in der Anzeige
und std::string
als Länge des Strings in Buchstaben. Hier eine gemeinsame Überschreibung für beide Funktionen anzugeben
ist nicht sinnvoll möglich: die Konzepte “Länge in Pixel” und “Länge in Buchstaben” sind einfach zu unterschiedlich, als
dass man sie verheiraten sollte. In dem Fall muss also auf die Mehrfachvererbung verzichtet werden und der Entwurf sollte
nochmal einen langen, kritischen Blick ertragen müssen.