Mehrfachvererbung

Einführung

Anders als in anderen Sprachen wie C# oder Java besteht in C++ die Möglichkeit der Mehrfachvererbung, d.h. die Möglichkeit eine Klasse von mehreren abzuleiten. Dabei kann es leicht zu Mehrdeutigkeiten kommen, wenn beispielsweise zwei Klassen eine Funktion 'f' mit gleicher Deklaration implementieren und eine dritte Klasse beide Klassen als Basisklassen angibt. Beim Aufruf muss dann explizit festgelegt werden, welche Funktion gemeint ist. 

Deklaration

Soll eine Klasse von mehreren Basisklassen abgeleitet werden, dann sieht dies in etwa wie folgt aus:

class <derivedclass> : <baseclass1>, <baseclass2>, ... , <baseclassn>
{
	/* --- Implementierung weiterer Methoden ---
	...
	*/
};

Dabei kann natürlich für jede Basisklasse einer der drei Zugriffsbezeichner ('public', 'private' oder 'protected') vorangestellt werden, um den Zugriff auf die Member einzuschränken. Dazu folgendes Beispiel:

class A
{
public:
	virtual char id(void) {return 'A';}
	virtual int f(void) {return 2;}
	virtual void g(int) {}
	virtual void h(void) {}
};

class B
{
public:
	virtual char id(void) {return 'B';}
	virtual double f(double d) {return d;}
	virtual int g(int i) {return i * i;}
	virtual int i(void) {return g(2);}
};

class C : public A, public B
{
};

void main(void)
{
	C c;
}

Es wird also eine Klasse 'C' deklariert, welche von seinen zwei Basisklassen 'A' und 'B' die Funktionen 'id', 'f', 'g', 'h' und 'i' erbt. 

Mehrdeutigkeiten durch gleichnamige Funktionen 

Von der Klasse 'C' lässt sich durchaus eine Instanz bilden, aber es lassen sich ohne Schwierigkeiten nur die Funktionen 'h' und 'i' aufrufen. Bei allen anderen kommt es unweigerlich zu Compilerfehlern: 

void main(void)
{
	C c;

	c.id();		//C2385: 'C::id' ist mehrdeutig
	c.f(2.0);	//C2385: 'C::f' ist mehrdeutig
	c.g(2);		//C2385: 'C::g' ist mehrdeutig
	c.h();
	c.i();
}

Obwohl beim Aufruf von 'f' eine Kommazahl übergeben wird, was eher für die Version aus der Klasse 'B' sprechen würde, kommt es auch hier zum Compilerfehler. Nun lässt sich bei jedem Aufruf aber explizit angeben, welche Funktion von welcher Basisklasse gewünscht ist. Dazu verwendet man einfach den Bereichsauflösungsoperator '::', der schon verwendet wurde, um Memberfunktionen außerhalb einer Klasse zu implementieren. Damit der letzte Quellcode wieder kompilierfähig ist, lassen sich wie folgt die einzelnen Methoden aufrufen:

void main(void)
{
	C c;

	c.A::id();
	c.B::f(2.0);
	c.A::g(2);		
	c.h();
	c.i();
}

Nun läuft alles erwartungsgemäß. Allerdings wäre es ja irgendwie wünschenswert, wenn sich zumindest die Funktion 'f' in beiden Varianten aufrufen ließe. Im Prinzip unterscheidet sich die Methode von 'A' und die von 'B' zumindest durch die Anzahl der Parameter. Dies reicht bereits aus um Funktionen zu überladen, ohne dass Mehrdeutigkeiten auftreten. Tatsächlich lässt sich dies ganz einfach erreichen. Man muss lediglich die Funktion 'f' für jede Klasse überschreiben und erhält dann eine einmal überladene Funktion 'f' in 'C'. Die Funktion in 'C' muss dann lediglich die entsprechende Funktion der Basisklasse aufrufen:

class C : public A, public B
{
public:
	//A::f
	int f(void) {return A::f();}
	
	//B::f
	double f(double d) {return B::f(d);}
};

Schon lässt sich 'f'  außerhalb von 'C' wieder ohne den Operator '::' aufrufen. Folgender Aufruf ist demnach wieder möglich:

C c;
c.f(2.0);
c.f();

Eine weitere Möglichkeit, die C++ bietet ist die Verwendung der bereits bekannten 'using'-Deklaration. Dabei muss festgelegt werden, welche Variante sich ohne den Operator '::' verwenden lässt:

class C : public A, public B
{
public:
	//A::f
	int f(void) {return A::f();}
	
	//B::f
	double f(double d) {return B::f(d);}

	//B::g
	using B::g;

	//A::id
	using A::id;
};

Nun lassen sich alle Methoden einmal mit und einmal ohne den Bereichsauflösungsoperator '::' aufrufen. Allerdings muss man bei den Funktionen 'id' und 'g' für 'C' festlegen, welche Methode (die von 'A' oder die von 'B') standardmäßig aufgerufen werden soll. Für die Funktion 'f' gilt diese Einschränkung nicht.

Mehrdeutigkeiten durch das mehrfaches Ableiten von einer Basisklasse

Zu einer anderen Form von Mehrdeutigkeiten kann es kommen, wenn eine Klasse eine Basisklasse mehr als einmal erbt. Dies ist natürlich nur möglich, wenn eine Klasse von mehreren Klassen abgeleitet ist, von denen mindestens zwei zumindest von einer gemeinsamen Klasse erben. Dazu folgendes Beispiel einer solchen Klassenhierarchie: 

class A
{
public:
	A() {cout << "A::A()" << endl;}
	virtual ~A() {cout << "A::~A()" << endl;}

	virtual char id(void) {return 'A';}
};

class B : public A
{
public:
	B() {cout << "B::B()" << endl;}
	virtual ~B() {cout << "B::~B()" << endl;}
	virtual char id(void) {return 'B';}
};

class C : public A
{
public:
	C() {cout << "C::C()" << endl;}
	virtual ~C() {cout << "C::~C()" << endl;}
	virtual char id(void) {return 'C';}
};

class D : public A
{
public:
	D() {cout << "D::D()" << endl;}
	virtual ~D() {cout << "D::~D()" << endl;}
	
}; 

class E	: public B, public C, public D
{
public:
	E() {cout << "E::E()" << endl;}
	virtual ~E() {cout << "E::~E()" << endl;}
};

Die Konstruktoren und Destruktoren sollen hier zunächst einmal nicht weiter beachtet werden. Auch soll jetzt nicht einfach eine Instanz von 'E' gebildet werden, mit der dann einfach die Funktion 'id' aufgerufen wird, nur um festzustellen, dass dies erneut eine Mehrdeutigkeit auslöst. Letzteres würde sich nämlich kaum von dem vorigen Beispiel unterscheiden, für das ja bereits Lösungen genannt wurden. Hier entsteht allerdings augrund einer anderen Tatsache eine andere Mehrdeutigkeit, wenn auch mit zugegeben ähnlichem Hintergrund. Die Tatsache, dass die Klasse 'E'  (wenn auch auf indirektem Wege) dreimal von der Klasse 'A' abgeleitet wird, lässt unter ganz bestimmten Umständen ein neues Problem auftreten. Wenn nämlich eine Instanz von 'E' gebildet wird und ein 'A*'-Zeiger auf diese Instanz verweisen soll, dann tritt unweigerlich ein Compilerfehler auf. Dabei spielt es keine Rolle, ob die Instanz von 'E' zunächst explizit gecastet wird oder nicht:

E e;
A* pa1 = &e;		//C2594: Mehrdeutige Konvertierung von 'class E *' in 'class A *'
A* pa2 = (A*)&e;	//C2594: Mehrdeutige Konvertierung von 'class E *' in 'class A *'

In dieser Klassenhierarchie wird die Klasse 'A' insgesamt dreimal als Basisklasse von 'E' angegeben. Das Problem dabei ist, dass alle Klassen, die direkt von 'A' ableiten (also 'B', 'C' und 'D' ), eine Kopie aller benötigten Daten von 'A'  (eigene Membervariablen aber auch den Zeiger auf die 'vftable') beinhalten und somit jede dieser Klassen jeweils eigenen Speicher beansprucht um alle diese Daten dort zu speichern. So kann jede Kopie andere Daten enthalten. (Darauf werde ich im nächsten Abschnitt noch einmal zurückkommen.) Soll nun  'A*'-Zeiger auf eine Instanz der Klasse 'E' zeigen, dann besteht die Mehrdeutigkeit darin, dass nicht festgelegt ist, welche der drei Kopien von 'A' nun verwendet werden soll. Aus diesem Grunde, muss zunächst festgelegt werden, welche der drei Kopien von 'A' als Grundlage dienen soll. Man macht die Festlegung ganz einfach, indem man zunächst einen 'B*'-, 'C*' oder 'D*'-Zeiger auf die Instanz von 'E' zeigen lässt und diesen dann explizit oder auch implizit in einen 'A*'-Zeiger umwandelt. Soll nun etwa die Klasse 'D' als Grundlage dienen, dann müsste das Casting wie folgt durchgeführt werden:

E e;
A* pa1 = (D*)&e;	//implizites Casting von 'D*' nach 'A*'
A* pa2 = (A*)(D*)&e;	//explizites Casting von 'D*' nach 'A*'

Ruft man nun über einen der Zeiger die Methode 'id' auf, dann wird die Methode von 'A' aufgerufen, da weder 'E' noch 'D' die Methode implementieren. Somit zeigt der Funktionszeiger der "vftable" auf die Funktion 'id' der Klasse 'A'. Würde man hingegen die Klasse 'B' oder 'C' anstatt 'D' in obigen Aufruf verwenden, dann würde ein Aufruf der Funktion 'id' über den entsprechenden Zeiger die Variante von 'B' oder 'C' aufrufen.

Die Konstruktoren und Destruktoren wurden mit Ausgaben versehen. Daran kann man erkennen in welcher Reihenfolge Konstruktoren und Destruktoren aufgerufen werden. Bildet man also eine Instanz von 'E', die kurz darauf wieder abgebaut wird, dann erscheinen folgende Ausgaben auf dem Bildschirm:

A::A()
B::B()
A::A()
C::C()
A::A()
D::D()
E::E()
E::~E()
D::~D()
A::~A()
C::~C()
A::~A()
B::~B()
A::~A()

Vergleicht man die Ausgabe mit der Deklaration der gesamten Klassenhierarchie, dann kommt man schnell hinter die Regeln der Aufrufreihenfolge. Zunächst wird die Liste der Basisklassen innerhalb der Deklaration von 'E' von links nach rechts durchlaufen (also erst 'B', dann 'C' und schließlich 'D'). Von den Klassen aus dieser Liste wird dann jeweils zunächst der Konstruktor der Klasse aufgerufen, die jeweils in der Hierarchie ganz oben steht (also jeweils 'A'). Zuletzt wird dann jeweils der eigene Konstruktor (also von 'B', 'C' und 'D') aufgerufen. Schließlich folgt dann der Aufruf des Konstruktors der abgeleiteten Klasse 'E'. Beim Abbau der Instanz wird dann schließlich genau der umgekehrte Weg verfolgt. Da für jede der direkten Basisklassen von 'E' (also 'B', 'C' und 'D') jeweils eine Kopie aller Daten von 'A' besteht wird auch jeweils der Konstruktor von 'A'  aufgerufen.

Virtuelle Basisklassen

Erweitert man nun die Klasse 'E' um eine eigene Variante der Funktion 'id', die einfach entsprechend den der anderen Klassen einfach den Wert 'E'  zurückgibt, dann erkennt man, dass es tatsächlich immer noch nicht möglich ist, einen 'A*'-Zeiger ohne voriges Casting auf eine Instanz von 'E' zeigen zu lassen. Obwohl nun keine Mehrdeutigkeit im Bezug auf die Funktion 'id' vorliegt, lässt sich immer noch aufgrund der drei existierenden Kopien von 'A', kein eindeutiges direktes Casting durchführen. Dies ist tatsächlich das einzige Hindernis, dass es gilt zu überwinden um ein eindeutiges Casting durchführen zu können. Dazu muss lediglich angegeben werden, dass lediglich eine Kopie von 'A' für alle Klassen, die direkt von ihr ableiten und alle gemeinsam in einer Klassenhierarchie vertreten sind, zur Verfügung steht.  Zu diesem Zweck besteht die Möglichkeit des virtuellen Ableitens mit Hilfe des Schlüsselwortes 'virtual'. Wird es bei der jeweiligen Basisklasse mit angegeben, dann existiert innerhalb der Klassenhierarchie tatsächlich nur eine Kopie und es wird der Speicherplatz für weitere Kopien eingespart und zudem die Möglichkeit geboten, Mehrdeutigkeiten auszuschließen. So sähe die Änderung dann in unserem Beispiel aus:

class A
{
public:
	A() {cout << "A::A()" << endl;}
	virtual ~A() {cout << "A::~A()" << endl;}

	virtual char id(void) {return 'A';}
};

class B : public virtual A
{
public:
	B() {cout << "B::B()" << endl;}
	virtual ~B() {cout << "B::~B()" << endl;}
	virtual char id(void) {return 'B';}
};

class C : public virtual A
{
public:
	C() {cout << "C::C()" << endl;}
	virtual ~C() {cout << "C::~C()" << endl;}
	virtual char id(void) {return 'C';}
};

class D : public virtual A
{
public:
	D() {cout << "D::D()" << endl;}
	virtual ~D() {cout << "D::~D()" << endl;}
	
}; 

class E	: public B, public C, public D
{
public:
	E() {cout << "E::E()" << endl;}
	virtual ~E() {cout << "E::~E()" << endl;}
	virtual char id(void) {return 'E';}
};

Nun existiert nur noch eine Kopie aller Daten von 'A', d.h. alle Klassen, die von 'A' virtuell ableiten und selbst in einer gemeinsamen Klassenhierarchie eingebunden sind, teilen sich einen gemeinsamen Bereich, in dem die Daten unterkommen. Besäße 'A' eine Membervariable, die in jedem Konstruktor anders initialisiert wird, so würde man den Unterschied schnell merken. Während bei der ersten Variante ('A' als nicht-virtuelle Basisklasse) jede Klasse einen individuellen Wert in der Variablen speichern kann, überschreibt jeder Konstruktoraufruf bei der zweiten Variante ('A' als virtuelle Basisklasse) den Wert der Membervariablen. So trägt die Membervariable bei der zweiten Variante am Ende den Wert, den der zuletzt aufgerufene Konstruktor dort speichert. Ein weiterer Unterschied wird deutlich wenn man nun eine Instanz der Klasse 'E' bildet und dabei die Ausgaben betrachtet:

A::A()
B::B()
C::C()
D::D()
E::E()
E::~E()
D::~D()
C::~C()
B::~B()
A::~A()

Die Reihenfolge der Aufrufe im Vergleich zu der vorigen Variante ist gleich geblieben. Allerdings wird nun der Konstruktor sowie der Destruktor von 'A' nur noch einmal aufgerufen.

Für die nächsten Lektionen

Dies war eine Einführung in die Mehrfachvererbung. Die Mehrfachvererbung ermöglicht es einer Klasse die Funktionalität mehrerer Klassen zu übernehmen und diese wahlweise noch zu erweitern. Das Problem sind die dabei leicht auftretenden Mehrdeutigkeiten, die auf verschiedene Art und Weise behoben werden oder umgangen werden können. Diese Möglichkeiten wurden hier aufgezeigt. Die Mehrfachvererbung von Klassen ist wie bereits erwähnt nur in C++ möglich. Andere Sprachen unterstützen nur das Erben von mehreren Schnittstellen (Schnittstellen sind im Prinzip Klassen, die nur aus rein virtuellen Funktionen bestehen). Mehrfachvererbung kommt bei vor allem der ATL zum tragen. Zuvor sollen jedoch noch Klassenvorlagen (Templates) vorgestellt werden.

Zurück Nach oben Weiter