Virtuelle Ableitungen

Das Problem

Wir hatten uns zuvor das Diamant-Problem angesehen und festgestellt, dass eine durchdachte Klassenhierarchie viele Probleme lösen kann. Nun wollen wir uns ansehen, wie die Datentypen BluRayPlayer, FullHDTelevision und BluRayFullHDTelevision so miteinander verknüpft, dass die Oberklassen von BluRayFullHDTelevision bereits vollständige Geräte sind - also von PowerConsumer abgeleitet sind.

Wir graben also wieder die ursprüngliche Deklaration von BluRayFullHDTelevision aus

class BluRayFullHDTelevision 
  : public FullHDTelevision
  , public BluRayPlayer
{
public:
  BluRayFullHDTelevision()
    : FullHDTelevision( 3, 250 )
  {
  }
};

und stehen nun erneut vor der Frage, wie wir das Diamant-Problem lösen wollen: FullHdTelevision und BluRayPlayer haben jeweils ihr eigenes Netzteil eingebaut, denn sie sind beide von PowerConsumer abgeleitet. Weiterhin muss die Klassenhierarchie so aufgebaut sein, dass man eine Funktion schreiben kann, die als Parameter einen BluRayPlayer & bekommt und beim BluRayPlayer, wie auch beim BluRayFullHDTelevision so Zugriff auf das einzige vorhandene Netzteil erhält und man weiterin eine Funktion schreiben kann, die als Parameter einen FullHDTelevision & erhält und beim FullHDTelevision, wie auch beim BluRayFullHDTelevision genauso Zugriff auf das einzige vorhandene Netzteil erhält. Im Fall des BluRayFullHDTelevision muss es sich also um das gleiche Netzteil (also um die gleiche Instanz der Klasse PowerConsumer) handeln - unabhängig davon, ob man das BluRayFullHDTelevision als BluRayPlayer oder als FullHDTelevision referenziert.

Hier kommen sogenannte virtuelle Ableitungen ins Spiel.

Die Lösung

Bisher sehen unsere Oberklassen wie folgt aus:

class BluRayPlayer : public PowerConsumer
{
private:
  bool DiscTrayOpen;
 
public:
  BluRayPlayer()
  : PowerConsumer( 1, 35 )
  {
    DiscTrayOpen = false;
  }
};
 
class FullHDTelevision 
  : public Display
  , public PowerConsumer
{
public:
  FullHDTelevision( int minWatts, int maxWatts )
  : Display( 1920, 1080 )
  , PowerConsumer( minWatts, int maxWatts )
  {}
};

Die Aussage 'ist ein PowerConsumer' trifft auf beide Geräte zu. Die direkte Ableitung lötet das Netzteil gewissermaßen fest in das Gerät ein. Die virtuelle Ableitung löst nun das Netzteil aus dem Gerät und sagt gewissermaßen folgendes aus: 'ein PowerConsumer wird mitgeliefert'. Stellen wir uns einen BluRayPlayer mit externen Netzteil vor: das Gerät funktioniert nur, wenn das Netzteil mitgeliefert wird. Eine Instanz von BluRayPlayer wird also immer mit Netzteil erzeugt und wenn die Klasse verwendet wird, ist garantiert, dass ein Netzteil vorhanden ist und der BluRayPlayer Strom konsumiert. Dennoch ist das Netzteil nicht fest mit dem Player verlötet, sondern kann ausgetauscht werden.

So lassen sich auch Basisklassen aus den Objekten lösen:

class BluRayPlayer 
  : virtual public PowerConsumer        // <- virtuelle Ableitung
{
private:
  bool DiscTrayOpen;
 
public:
  BluRayPlayer()
  : PowerConsumer( 1, 35 )
  {
    DiscTrayOpen = false;
  }
};
 
class FullHDTelevision 
  : public Display
  , virtual public PowerConsumer        // <- virtuelle Ableitung
{
public:
  FullHDTelevision( int minWatts, int maxWatts )
  : Display( 1920, 1080 )
  , PowerConsumer( minWatts, int maxWatts )
  {}
};

Anschließend wird der BluRayPlayer unverändert wie zuvor deklariert:

class BluRayFullHDTelevision 
  : public FullHDTelevision
  , public BluRayPlayer
{
public:
  BluRayFullHDTelevision()
    : FullHDTelevision( 3, 250 )
    , PowerConsumer( 3, 250 )
  {
  }
};

Die beiden Klassen FullHDTelevision und BluRayPlayer verlangen, dass ein Netzteil mitgeliefert wird. Durch die virtuelle Ableitung ist aber ausgesagt, dass sie sich ein Netzteil mit einem anderen Gerät teilen können. Wäre das nicht möglich, müsste jeder seinen eigenen PowerConsumer haben, was einer Standard-Ableitung entspricht.

Unser FullHD-Fernseher-Konstructor verlangt nun die Information, wieviel Leistung er aufnimmt. Er wird aber den virtuellen Konstruktor damit nicht mehr aufrufen - man könnte einen Default-Konstruktor anlegen, der öffentlich nicht sichtbar ist, sondern nur von abgeleiteten (protected) Klassen gerufen werden kann - wie beispielsweise der Klasse BluRayFullHDTelevision. Der virtuelle Konstruktor muss immer auch von der Klasse aufgerufen werden, in der sich mehrere virtuelle Ableitungen treffen. BluRayFullHDTelevision muss also PowerConsumer eindeutig bestücken.

Nun sieht unsere Klasse so aus:

Bedeutung von virtuellen Ableitungen

Bei einer normalen Ableitung erzeugt eine Instanz der Ableitung einen Speicherbereich in dem die Daten der Basisklasse und die Erweiterungen der Ableitung gespeichert werden können. Die Basisklasse und die Ableitung liegen also direkt hintereinander im Speicher. Bei der Mehrfachvererbung liegen so mehrere Klassen hintereinander im Speicher. Da BluRayPlayer und FullHDTelevision beide virtuell von PowerConsumer abgeleitet sind, liegen wird für beide Klassen nur ein PowerConsumer erzeugt und dieser in die Klasse BluRayFullHDTelevision gelegt. Die beiden Klassen BluRayPlayer und FullHDTelevision greifen nun über ihren Virtual-Zeiger auf die PowerConsumer-Instanz von BluRayFullHDTelevision zu und haben selbst keinen eigenen mehr: