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
#include <iostream>
class Base
{
public:
virtual void sayHello() const = 0;
};
class Left : virtual public Base
{
public:
void sayHello() const
{
std::cout << "Hello from the left\n";
}
};
class Right : virtual public Base
{
public:
void sayHello() const
{
std::cout << "Hello from the right\n";
}
};
class Join : public Left, public Right
{
public:
void sayHello() const
{
std::cout << "Hello from Join\n";
}
};
int main()
{
Join j;
j.sayHello();
}
Kollision aufgrund zufällig gleicher Signatur
#include <iostream>
class Left
{
public:
virtual void sayHello() const
{
std::cout << "Hello from Left\n";
}
};
class Right
{
public:
virtual void sayHello() const
{
std::cout << "Hello from Right\n";
}
};
class Join : public Left, public Right
{
public:
void sayHello() const
{
std::cout << "Override\n";
}
};
void leftSayer(Left const &l)
{
l.sayHello();
}
void rightSayer(Right const &r)
{
r.sayHello();
}
int main()
{
Join j;
j.sayHello();
leftSayer(j);
rightSayer(j);
}
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.