Mehrfachvererbung ist ein gutes Werkzeug in C++, aber wie viele gute Werkzeuge können sie auch falsch verwendet werden. Das Diamant-Problem ist ein bekanntes Problem, dass man kennen muss, wenn man professionell programmieren möchte. Die Alternative ist, Sprachen zu verwenden, die Mehrfachvererbung untersagen aufgrund der Möglichkeit Mehrfachvererbung falsch zu verwenden. Beispiele hierfür sind die populären Sprachen Java und C# 1). Damit verliert man allerdings auch die Möglichkeiten, die Mehrfachvererbung bietet.
Schauen wir uns das Beispiel des FullHDTelevision
der Mehrfachvererbung nocheinmal an: er ist ein Display
und er ist ein PowerConsumer
.
Definieren wir nun einen BluRay-Player:
class BluRayPlayer : public PowerConsumer { private: bool DiscTrayOpen; public: BluRayPlayer() : PowerConsumer( 1, 35 ) { DiscTrayOpen = false; } };
Nachdem wir nun einen FullHDFernseher in der Mehrfachvererbung entwickelt haben und nun auch eine Klasse BluRayPlayer haben, könnte man auf die Idee kommen, ein Produkt FullHD-Fernseher mit eingebautem BluRayPlayer zu entwickeln:
class BluRayFullHDTelevision : public FullHDTelevision , public BluRayPlayer { public: BluRayFullHDTelevision() : FullHDTelevision( 3, 250 ) { } };
So schnell haben wir nicht nur die neue Klasse fertig, die Fernseher mit BluRayPlayer und genauso schnell haben wir semantischen Unsinn2) und damit ein Diamond-Problem erzeugt.
Was ist passiert? Der FullHDTelevision
ist ein PowerConsumer
, der zwischen 3 und 250 Watt verbraucht. Und hier kommt das Diamantproblem ins Spiel: Der BluRayPlayer
ist ein PowerConsumer
, der zwischen 1 und 35 Watt verbraucht. Die Daten für PowerConsumer
tauchen also doppelt auf.
Dieses Problem lässt sich nicht mehr mit using beschreiben, denn schließlich ist ein Gerät auch nur genau ein Stromverbraucher und nicht zwei.
Es gibt zwei Möglichkeiten das Diamond-Problem zu lösen. Man kann C++ das Problem zur Laufzeit lösen lassen oder man benutzt seinen Kopf - im Idealfall bevor man etwas falsches programmiert hat.
Die effektivere Lösung sind interne Basisklassen. Hierfür setzen wir die eigentlichen Objekte möglichst spät zusammen. Das bedeutet in unserem Fall, dass wir zunächst interne Klassen erzeugen, die keinen Stromverbrauch verwalten:
class BluRayPlayerWithoutPowerSupply { private: bool DiscTrayOpen; public: BluRayPlayerWithoutPowerSupply() { DiscTrayOpen = false; } }; class BluRayPlayer : public BluRayPlayerWithoutPowerSupply , public PowerConsumer { public: BluRayPlayer() : PowerConsumer( 1, 35 ) { } };
class FullHDTelevisionWithoutPowerSupply : public Display { public: FullHDTelevisionWithoutPowerSupply() : Display( 1920, 1080 ) {} }; class FullHDTelevision : public FullHDTelevisionWithoutPowerSupply , public PowerConsumer { public: FullHDTelevision( int minWatts, int maxWatts ) : PowerConsumer( minWatts, maxWatts ) {} };
class BluRayFullHDTelevision : public FullHDTelevisionWithoutPowerSupply , public BluRayPlayerWithoutPowerSupply , public PowerConsumer { public: BluRayFullHDTelevision() : PowerConsumer( 4, 285 ) { } };
FullHDTelevision
und BluRayPlayer
funktionieren identisch zu den Varianten, wie sie vorher waren. Die beiden Zwischenklassen FullHDTelevisionWithoutPowerSupply
und BluRayPlayerWithoutPowerSupply
sind für den Entwickler außerhalb dieser Implementierung uninteressant. Sie erlauben aber die Klasse BluRayFullHDTelevision
zusammenzubauen, ohne das Diamond-Problem hervorzurufen.
Alle Geräte sind genau einmal von PowerConsumer
abgeleitet und können an Funktionen übergeben werden, die einen PowerConsumer
als Parameter wünschen. Die FullHD-Bildschirme können über FullHDTelevisionWithoutPowerSupply
übergeben werden. Methoden, die ein FullHDTelevisionWithoutPowerSupply
können also mit einem FullHDTelevision
und einem BluRayFullHDTelevision
gefüttert werden.
Nun haben wir auch das richtige Gerät beschrieben: Statt eines Displays mit Netzteil und einem BluRay-Laufwerk mit Netzteil haben wir ein Gerät beschrieben, dass aus einem Display, einem BluRay-Laufwerk und genau einem Netzteil besteht.
Im Fazit der ersten Lösung wurde gezeigt, dass mit einer überlegten Anordnung der Ableitungshierarchie das Diamantproblem gelöst wurde und soweit alle wichtigen Möglichkeiten mit einer passenden Basisklasse als Parameter gegeben sind. Alle?
Wir haben zwei verschiedene Bildschirmtypen und wir können mit FullHDTelevisionWithoutPowerSupply
die beiden dazugehörigen Typen BluRayFullHDTelevision
und FullHDTelevision
zusammenfassen. Aber ein FullHDTelevisionWithoutPowerSupply
erlaubt - wie der Name schon sagt - keinen Zugriff auf die Klasse PowerConsumer
, obwohl sowohl BluRayFullHDTelevision
als auch FullHDTelevision
jeweils ein PowerConsumer
sind.
Man könnte dieses Beispiel nun so ändern, dass ein FullHDTelevision
von PowerConsumer
abgeleitet wird und BluRayFullHDTelevision
von BluRayPlayerWithoutPowerSupply
und den PowerConsumer
von FullHDTelevision
im Konstruktor auf die richtigen Werte korrigiert. Dann hätten wir aber das gleiche Problem mit dem BluRayPlayer
, bei dem es dann keine Oberklasse gäbe, der einen BluRayPlayer
inkl. PowerConsumer
abfragt.
Dies ist mit virtuellen Ableitungen möglich.