Das Diamant-Problem

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.

Das Problem

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.

Erläuterung

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.
Das Diamant-Problem

Lösungen

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.

Lösung 1: interne Basisklassen

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:

Implementierung

Der BluRayPlayer
class BluRayPlayerWithoutPowerSupply
{
private:
  bool DiscTrayOpen;
 
public:
  BluRayPlayerWithoutPowerSupply()
  {
    DiscTrayOpen = false;
  }
};
 
class BluRayPlayer : public BluRayPlayerWithoutPowerSupply
                   , public PowerConsumer
{
public:
  BluRayPlayer()
  : PowerConsumer( 1, 35 )
  {
  }
};
Der FullHDTelevision
class FullHDTelevisionWithoutPowerSupply 
  : public Display
{
public:
  FullHDTelevisionWithoutPowerSupply()
  : Display( 1920, 1080 )
  {}
};
 
class FullHDTelevision 
  : public FullHDTelevisionWithoutPowerSupply
  , public PowerConsumer
{
public:
  FullHDTelevision( int minWatts, int maxWatts )
   : PowerConsumer( minWatts, maxWatts )
  {}
};
Das Kombi-Gerät
class BluRayFullHDTelevision 
  : public FullHDTelevisionWithoutPowerSupply
  , public BluRayPlayerWithoutPowerSupply
  , public PowerConsumer
{
public:
  BluRayFullHDTelevision()
    : PowerConsumer( 4, 285 )
  {
  }
};

Fazit

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.

Lösung 2: virtuelle Ableitungen

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.

1) Ich durfte soeben das Diamantproblem in C# lösen, da man Interfaces mehrfach implementieren kann. Damit schafft C# lediglich die Möglichkeiten der Mehrfachvererbung ab, während es die Probleme beibehält. siehe Beispielprogramm
2) Semantik beschreibt die Bedeutung einer Formulierung - im Gegensatz zur Syntax, die die richtige Verwendung bzw. Reihenfolge der Worte beschreibt