Twitter und Facebook-Anbindung
X
Tweet Follow @twitterapi
!!! Anbindung an twitter und facebook öffnen !!!

Wenn Ihnen mein Online-Buch gefällt,
dann bedanken Sie sich doch mit einer kleinen Spende...

22 Mehrfachvererbung

22 Mehrfachvererbung

Mit Mehrfachvererbung ist gemeint, dass eine Klasse von mehr als nur einer Basisklasse erben kann. Dies klingt zwar auf den ersten Anhieb ganz genial, aber der erfahrene Programmierer macht einen weiten Bogen um diesen Mechanismus, weil er einem unterm Strich mehr Probleme macht, als er Nutzen bringt. Der Vollständigkeit halber, möchte ich dieses Thema aber nicht unter den Tisch fallen lassen und werde ganz detailliert erklären, warum man sich mit diesem Mechanismus das Leben unnötig schwer macht.

Zum Seitenanfang
Zum Inhaltsverzeichnis

22.1 Ein harmloses Beispiel

22.1 Ein harmloses Beispiel

In meinem Beispiel erbt die Klasse "CCadillac" von "„CAuto" und von "CCabrio". Somit besitzt die Klasse alle Eigenschaften eines Autos und eines Cabrios.

Veranschaulicht eine Mehrfachvererbung

Im der Header-Datei würde dies dann folgendermaßen aussehen.

 1
 2
 3
					
class CCadillac : public CAuto, public CCabrio {
	// Prototypen der Klasse
};
					

Wie Sie in Zeile 1 sehen können, ist es also durchaus möglich, mehrere Basisklassen, mit Komma getrennt, anzugeben. Die Klasse kann dann auf die Attribute und Methoden beider Basisklassen zugreifen und auch über den entsprechenden Namespace, auf virtuelle Methoden zugreifen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

22.3 Virtuelles Erben

22.3 Virtuelles Erben

Ich hatte schon einmal angedeutet, dass es möglich ist, auch virtuell zu erben, hatte aber nicht weiter erklärt, was damit gemeint ist und wozu es gut ist. Dies werde ich an dieser Stelle nachholen.

Sie hatten ja feststellen müssen, dass es mit der Mehrfachvererbung einige Schwierigkeiten gibt. Mit einer virtuellen Vererbung kann man ein paar dieser Schwierigkeiten umschiffen. Ein großer Nachteil war ja, dass es zu Dopplungen in abgeleiteten Klassen kommt, die bei einer diamantförmigen Vererbung störend ist. Wenn man aber die mittleren Klassen virtuell von der obersten Klasse erben lässt, hat man dieses Problem nicht und das Speicherlayout ähnelt dem, welches wir uns zuerst gewünscht hätten. Ich passe also das eben genannte Beispiel leicht an und das ganze sieht dann wie folgt aus.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
					
class CTransportmittel {
	public:
		int m_iPositionX;
		int m_iPositionY;
		int m_iPositionZ;
};



class CFahrzeug : public virtual CTransportmittel {
	public:
		int m_iReader;
};

class CFlugzeug : public virtual CTransportmittel {
	public:
		int m_iTriebwerke;
};

class CSchiff : public virtual CTransportmittel {
	public:
		int m_iSegel;
};



class CWasserFlugzeug : public CFahrzeug, public CFlugzeug, public CSchiff {
	public:
		int m_iPontons;
};

// ...

printf("CTransportmittel %i\n", sizeof(CTransportmittel));
printf("CFahrzeug        %i\n", sizeof(CFahrzeug));
printf("CFlugzeug        %i\n", sizeof(CFlugzeug));
printf("CSchiff          %i\n", sizeof(CSchiff));
printf("CWasserFlugzeug  %i\n", sizeof(CWasserFlugzeug));
					

Ausgabe:

CTransportmittel 12
CFahrzeug        20
CFlugzeug        20
CSchiff          20
CWasserFlugzeug  40
		

Wie Sie sehen, hat sich vom Quelltext her nicht viel geändert, aber die Größen der Klassen haben sich wesentlich verändert. Wie kommen die Werte Zustande? Die Größe der Klasse "CTransportmittel" ist gleich geblieben und somit auch nachvollziehbar, aber die Klassen "CFahrzeug", "CFlugzeug", "CSchiff" und "CWasserFlugzeug" sind auf einmal anders.

Die drei mittleren Klassen, welche virtuell erben, haben jetzt auch noch einen Pointer zusätzlich auf eine eigene virtuelle Tabelle hinzubekommen. Rechnen wir mal nach. Zwölf Byte für die Basisklasse plus drei mal acht Byte für die mittleren Klassen (Attribut plus VT-Pointer) sind zusammen 36. Jetzt sagte ich noch, dass auch die Klasse "CWasserFlugzeug" einen Pointer auf die VT bekommt, plus die vier Byte für das eigene Attribut. Da hätten wir eine Gesamtsumme von 42. Die Ausgabe hat aber gezeigt, dass die Klasse nur 40 Byte groß ist. Wie kann das sein? Um dies zu verstehen, habe ich in folgender Grafik das Speicherlayout vereinfacht dargestellt.

Speicherlayout bei einer virtuellen Vererbung

Wie Sie sehen können, teilen sich "CFahrzeug" und "CWasserFlugzeug" einen VT-Pointer. Dies ist für dieses einfache Beispiel möglich, denn der Visual Studio Compiler ist extrem effizient und optimiert so weit wie er kann. Mit anderen Compilern oder komplexeren Strukturen, kann die Sache aber schon ganz anders aussehen.

Sie merken also, dass das Speicherlayout sehr komplex werden kann und es ist auch meistens so, dass die Klassen nicht mehr zusammenhängend im Speicher stehen, was nicht gerade Cachefreundlich ist. Wir umgehen damit zwar jetzt die Dopplungen aber wir erkaufen uns dies durch zwei Nachteile. Zum einen kommen jetzt zusätzliche virtuelle Tabellen hinzu, welche wiederum Speicher benötigen und für jedem Zugriff eine Indirektionsstufe mehr benötigen (es wird langsamer). Zum anderen ist es jetzt nicht mehr möglich, eine Instanz der Klassen "CFahrzeug", "CFlugzeug" und "CSchiff" zu erzeugen, da sie jetzt abstrakte Klassen sind. Wie sieht es denn jetzt mit dem Polymorphismus aus? Der Upcast funktioniert wieder einwandfrei, aber der Downcast bleibt nach wie vor ein großes Problem und die Argumentation fällt wieder genauso aus. Es wird sogar noch schlimmer. Bei einer einfachen Mehrfachvererbung war es zu Mindestens möglich, mit Hilfe von Zeigern auf Objekte eine Hierarchieebene nach unten zu casten. Da die Positionsattribute jetzt aber in die oberste Basisklasse gewandert sind, ist auch dies jetzt nicht mehr möglich.

Das Fazit ist also, virtuelle Vererbung scheint zwar im ersten Augenblick die Lage zu entschärfen, aber bei näherem Betrachten fängt man sich noch mehr Ärger ein. Sie sollten sich also merken, dass egal wie Sie es anstellen, eine Mehrfachvererbung immer ungünstig ist. Meistens kann man diese Problematiken mit einer gekonnten Aggregation/Komposition umgehen. Die von mir genannten Beispiele hinken sowieso, da man sich bei Computerspielen eher für s.g. Leichtgewichtsobjekte entscheidet und das Zeichnen nicht den Klassen überlässt, sondern eine eigene Renderklasse dafür implementiert. Ich werde zu einem späteren Zeitpunkt diese Thematik wieder aufgreifen, wenn es um die Design Pattern geht.

Zum Seitenanfang
Zum Inhaltsverzeichnis

22.2 Diamantförmige Vererbung

22.2 Diamantförmige Vererbung

Bisher habe ich Ihnen zwar prinzipiell gezeigt, wie eine Mehrfachvererbung vom Prinzip her funktioniert, aber ich konnte Ihnen noch nicht zeigen, welche negativen Konsequenzen sich daraus ergeben und warum dieses Konzept in vielen anderen Sprachen nicht implementiert wird.

Mit einer diamantförmigen Vererbung ist gemeint, dass es irgendwo eine Basisklasse gibt, wovon zwei oder mehr andere Klassen erben. Anschließend gibt es eine weitere Klasse, welche genau von diesen abgeleiteten Klassen erbt. Im Klassendiagramm ergibt sich das eine Art Raute oder Diamant. Dies soll folgendes Beispiel demonstrieren.

Diamantförmige Vererbung

Wie Sie sehen, habe ich mir ein Beispiel ausgedacht, welches zum einen komplexer ist und zum anderen vom bisherigen Beispiel abweicht. Dies war notwendig, damit ich Ihnen alle wichtigen Aspekte n6AUML,her bringen kann. Schauen Sie sich zunächst die Klassendefinitionen an.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
					
class CTransportmittel {
	public:
		int m_iPositionX;
		int m_iPositionY;
		int m_iPositionZ;
};



class CFahrzeug : public CTransportmittel {
	public:
		int m_iReader;
};

class CFlugzeug : public CTransportmittel {
	public:
		int m_iTriebwerke;
};

class CSchiff : public CTransportmittel {
	public:
		int m_iSegel;
};



class CWasserFlugzeug : public CFahrzeug, public CFlugzeug, public CSchiff {
	public:
		int m_iPontons;
};
					

Man würde jetzt ganz spontan von folgendem Speicherlayout ausgehen.

Gewünschtes Speicherlayout bei einer diamantförmigen Vererbung

Schauen wir uns doch mit einem kleinen Testprogramm an, ob dies tatsächlich der Wahrheit entspricht.

 1
 2
 3
 4
 5
					
printf("CTransportmittel: %i\n", sizeof(CTransportmittel));
printf("CFahrzeug:        %i\n", sizeof(CFahrzeug));
printf("CFlugzeug:        %i\n", sizeof(CFlugzeug));
printf("CSchiff:          %i\n", sizeof(CSchiff));
printf("CWasserFlugzeug:  %i\n", sizeof(CWasserFlugzeug));
					

Ausgabe:

CTransportmittel: 12
CFahrzeug:        16
CFlugzeug:        16
CSchiff:          16
CWasserFlugzeug:  52
		

Ich denke, die ersten vier Werte sind nachvollziehbar. Fahrzeug, Flugzeug und Schiff erben die zwölf Byte der Basisklasse und bringen selbst noch jeweils vier Byte hinzu. Rechnet man jetzt aber vier Byte mal drei erbende Klassen, plus zwölf Byte der obersten Basisklasse, plus vier Byte für die Pontons, erhält man 28. Scheinbar sieht das Layout des Speichers doch anders aus.

Speicherlayout bei einer diamantförmigen Vererbung

Diese Abbildung gleicht schon eher der Realität. Rechnen wir mal nach. Sechzehn mal drei erbende Klassen plus vier Byte für die Pontons, ergibt 52. Ihnen sollte jetzt auffallen, dass es drei mal die Attribute für die Positionsangabe gibt. Wenn man also von außen auf ein Objekt der Klasse zugreift und man die Position ändern will, fragt sich jetzt, auf welche Positionsangabe man Zugriff hat, bzw. wie man diese unterscheiden kann. Genau diese Frage stellt sich der Compiler auch und wird einen normalen Zugriff bemängeln. Deshalb muss man immer den Namensraum mit angeben. Dies könnte dann wie folgt aussehen.

 1
 2
					
CWasserFlugzeug oWasserFlugzeug;
oWasserFlugzeug.CSchiff::m_iPositionX = 10;
					

Diese Schreibweise sieht nicht nur komisch aus, sie ist auch unpraktisch. Stellen Sie sich vor, Sie wollten tatsächlich mit dieser Klasse arbeiten und das Objekt würde sich bewegen. Um alles konsistent zu halten, müssten Sie also jeweils drei mal die X, Y und Z Position ändern. Im Zweifelsfall ist die Vererbungsstruktur noch größer und Sie wissen gar nicht, wie oft es diese Positionsangabe noch gibt.

Sie sehen also schon an diesem trivialen Beispiel, dass es zu großen Unannehmlichkeiten kommen kann. Dies ist der Grund, warum viele Programmiersprachen auf dieses Konzept verzichten. In Java ist z.B. eine Mehrfachvererbung nur in dem Sinne möglich, dass man von einer normalen und dann nur noch von zusätzlichen vollständig abstrakten Klassen (Interfaces) erbt. In diesem Fall kommt es zu keinen Dopplungen, da vollständig abstrakte Klassen weder Attribute noch Methoden bereitstellen. Es gibt, wie bereits erwähnt, nur Muster für Methoden, die in abgeleiteten Klassen zu implementieren sind.

Ein weiterer großer Nachteil ist, das man mit solchen Strukturen arge Probleme mit dem Polymorphismus bekommt. Der Upcast funktioniert in der Regel noch, aber spätestens der Downcast scheitert. Schauen wir uns folgenden Quelltext an.

 1
 2
 3
 4
 5
 6
 7
					
CWasserFlugzeug		oWasserFlugzeug;
CFlugzeug		oFlugzeug		= oWasserFlugzeug;
CTransportmittel	oTransportmittel	= oFlugzeug;

// Folgende Zeilen bringen einen Compilerfehler
oWasserFlugzeug					= static_cast<CWasserFlugzeug>(oFlugzeug);
oFlugzeug					= static_cast<CFlugzeug>(oTransportmittel);
					

Warum sind die Downcasts nicht mehr möglich? Zeile 6 funktioniert nicht, weil es keinen geeigneten benutzerdefinierten Konstruktor gibt. Sicher könnte man für die Klasse "CWasserFlugzeug" einen Konstruktor bauen, der sich aus einem Flugzeug erzeugen kann und in ihm kopiert man die Werte in die Positionsangaben für Fahrzeug, Flugzeug und Schiff, aber mit was initialisiert man dann "m_iReader", "m_iSegel" und "m_iPontons"? Man kommt ja an diese Informationen nicht mehr heran. Zeile 7 funktioniert aus den selben Gründen nicht. Auch hier würden wieder geeignete Konstruktoren fehlen, bei welchen noch mehr Informationen verloren gehen würden. Geeignete Konstruktoren lösen die Sache also nicht wirklich.

Wenn man jetzt Zeiger benutzt, kommt man um das Problem der Konstruktoren herum, da nur die Zeiger neu interpretiert werden müssen. Schauen wir uns ein Beispiel an.

 1
 2
 3
 4
 5
 6
 7
 8
					
CWasserFlugzeug*	pWasserFlugzeug		= new CWasserFlugzeug();
CFlugzeug*		pFlugzeug		= pWasserFlugzeug;
CTransportmittel*	pTransportmittel	= pFlugzeug;

pWasserFlugzeug					= static_cast<CWasserFlugzeug*>(pFlugzeug);
pFlugzeug					= static_cast<CFlugzeug*>(pTransportmittel);

delete pWasserFlugzeug;
					

Dieses Beispiel akzeptiert der Compiler. Zeile 5 und 6 funktionieren, welch die Klassen, auf welche die Zeiger verweisen, in einer direkten und eineindeutigen Vererbungshierarchie stehen. Eine Umwandlung von Transportmittel zurück zu einem Wasserflugzeug ist nicht möglich, da bei einem Zeiger keinerlei Typinformationen vorhanden sind und somit muss der Compiler davon ausgehen, dass es vorher kein Wasserfahrzeug war. Könnte ja ebenso gut ein Fahrzeug, Flugzeug oder Schiff gewesen sein.

Wenn man in den letzten Kapiteln aufgepasst hat, könnte man für genau letzten Fall ein reinterpretierenden Cast benutzen und der Compiler würde sich nicht beschweren, aber spätestens beim Freigeben des Wasserflugzeuges würde es böse krachen. Der Speicherbereich wird neu interpretiert und das hat zur folge, dass das Speicherlayout total durcheinander geworfen wird (ich verkneife mir an dieser Stelle ein Beispiel).

Sie sehen also, dass eine Mehrfachvererbung mehr Nachteile als Vorteile bringt. Man benötigt mehr Speicher, Variablen können doppelt auftreten und ein Polymorphismus ist nicht wirklich anwendbar. Wenn Sie also eine Mehrfachvererbung nutzen, dann bitte nur mit vollständig abstrakten Klassen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

© Copyright by Thomas Weiß, 2009 - 2012