Mehrfachvererbung und das Diamantproblem in C++
Lernen Sie Mehrfachvererbung und das Diamantproblem in C++, einschließlich virtueller Vererbung und Lösungsansätzen.
C++ unterstützt Mehrfachvererbung (multiple inheritance): Eine Klasse kann von mehr als einer Basisklasse erben. Diese Flexibilität bringt jedoch Komplexität mit sich – insbesondere, wenn dieselbe Basisklasse über verschiedene Vererbungszweige mehrfach geerbt wird. Dies führt zum sogenannten Diamond Problem (Diamantproblem). In diesem Artikel erklären wir, was das Diamond Problem ist, warum es entsteht, welche Symptome es zeigt und wie man es mithilfe von virtueller Vererbung (virtual inheritance) löst.
1. Was ist das Diamond Problem?
Betrachten Sie folgende Hierarchie:
Publication
/ \
EBook PrintedBook
\ /
HybridBook
HybridBook erbt sowohl von EBook als auch von PrintedBook.
Da beide wiederum von Publication erben, enthält HybridBook zwei separate
Publication-Teilobjekte.
Das Ergebnis: Mehrdeutigkeit, Duplikation und
zusätzlicher Speicherverbrauch.
2. Problematische (nicht-virtuelle) Vererbung
Sehen wir uns das Problem in der Praxis an:
#include <iostream>
#include <string>
using namespace std;
class Publication {
protected:
string title;
public:
Publication(string t) : title(t) {}
void PrintTitle() const { cout << "Titel: " << title << endl; }
};
class EBook : public Publication {
public:
EBook(string t) : Publication(t) {}
};
class PrintedBook : public Publication {
public:
PrintedBook(string t) : Publication(t) {}
};
class HybridBook : public EBook, public PrintedBook {
public:
HybridBook(string te, string tp) : EBook(te), PrintedBook(tp) {}
};
int main() {
HybridBook hb("E-Titel", "P-Titel");
// Mehrdeutigkeit: zwei verschiedene Publication-Teilobjekte
// hb.PrintTitle(); // FEHLER: 'PrintTitle' ist mehrdeutig
hb.EBook::PrintTitle(); // Titel: E-Titel
hb.PrintedBook::PrintTitle(); // Titel: P-Titel
}
Wie man sieht, enthält HybridBook zwei verschiedene Publication-Objekte.
Das führt zu Doppelungen und Mehrdeutigkeiten bei Methoden und Daten.
3. Die Lösung: Virtuelle Vererbung
Wenn beide Zwischenklassen (EBook und PrintedBook) dieselbe Basisklasse
virtuell erben, existiert in der untersten Klasse (HybridBook)
nur eine einzige gemeinsame Kopie der Basisklasse.
class Publication {
protected:
string title;
public:
Publication(string t) : title(t) { cout << "Publication()" << endl; }
void PrintTitle() const { cout << "Titel: " << title << endl; }
};
// HINWEIS: virtuelle öffentliche Vererbung
class EBook : virtual public Publication {
public:
EBook(string t) : Publication(t) { cout << "EBook()" << endl; }
};
class PrintedBook : virtual public Publication {
public:
PrintedBook(string t) : Publication(t) { cout << "PrintedBook()" << endl; }
};
class HybridBook : public EBook, public PrintedBook {
public:
HybridBook(string t)
: Publication(t), EBook(t), PrintedBook(t) {
cout << "HybridBook()" << endl;
}
};
Jetzt enthält HybridBook nur noch eine Publication-Instanz –
die Mehrdeutigkeit ist beseitigt:
int main() {
HybridBook hb("Gemeinsamer Titel");
hb.PrintTitle(); // Wird über eine einzige Publication-Instanz aufgerufen
}
Wichtig: Der am weitesten abgeleitete Typ (HybridBook)
ist immer verantwortlich für die Initialisierung der virtuellen Basisklasse.
4. Konstruktionsreihenfolge und virtuelle Basen
Bei virtueller Vererbung erfolgt die Konstruktion in folgender Reihenfolge:
- Zuerst die virtuellen Basisklassen (eine Kopie).
- Dann die Zwischenklassen (
EBook,PrintedBook). - Zum Schluss die abgeleitete Klasse (
HybridBook).
Nur die am weitesten abgeleitete Klasse initialisiert die virtuelle Basis:
class Publication {
public:
string title;
Publication(string t) : title(t) { cout << "Publication()" << endl; }
};
class EBook : virtual public Publication {
public:
EBook() : Publication("dummy-ignored") { cout << "EBook()" << endl; }
};
class PrintedBook : virtual public Publication {
public:
PrintedBook() : Publication("dummy-ignored") { cout << "PrintedBook()" << endl; }
};
class HybridBook : public EBook, public PrintedBook {
public:
HybridBook(string t) : Publication(t), EBook(), PrintedBook() {
cout << "HybridBook()" << endl;
}
};
int main() {
HybridBook hb("Gemeinsam");
cout << hb.title << endl; // "Gemeinsam"
}
5. Mehrdeutigkeit und Lösungen
Selbst mit virtueller Vererbung können Methoden mit demselben Namen Mehrdeutigkeiten verursachen. Lösungen:
- Namensraumqualifizierung: z. B.
obj.EBook::Foo(). - Override: Eine gemeinsame Methode in der abgeleiteten Klasse definieren.
- using-Deklaration: Eine gewünschte Methode aus der Basisklasse übernehmen.
struct A { void Info() const { cout << "A" << endl; } };
struct B : virtual A { void Info() const { cout << "B" << endl; } };
struct C : virtual A { void Info() const { cout << "C" << endl; } };
struct D : B, C {
void Info() const { cout << "D (vereinheitlicht)" << endl; }
};
6. Beispiel im Buchsystem
Ein realistischeres Beispiel:
BookBase als abstrakte Basisklasse,
EBook und PrintedBook erben virtuell,
und HybridBook kombiniert beide Preismodelle.
#include <iostream>
#include <string>
using namespace std;
class BookBase {
protected:
string title; string author;
public:
BookBase(string t, string a) : title(t), author(a) {}
virtual ~BookBase() = default;
virtual double GetBasePrice() const = 0;
virtual void Print() const {
cout << title << " - " << author << endl;
}
};
class EBook : virtual public BookBase {
protected:
double digitalPrice; double vat;
public:
EBook(string t, string a, double price, double v)
: BookBase(t,a), digitalPrice(price), vat(v) {}
double GetBasePrice() const override { return digitalPrice*(1+vat); }
};
class PrintedBook : virtual public BookBase {
protected:
double printBase; int pages; bool hardCover;
public:
PrintedBook(string t, string a, double base, int p, bool hard)
: BookBase(t,a), printBase(base), pages(p), hardCover(hard) {}
double GetBasePrice() const override {
double cost = printBase + pages*0.03;
if (hardCover) cost *= 1.1;
return cost;
}
};
class HybridBook : public EBook, public PrintedBook {
public:
HybridBook(string t, string a,
double digital, double vat,
double printBase, int p, bool hard)
: BookBase(t,a),
EBook(t,a,digital,vat),
PrintedBook(t,a,printBase,p,hard) {}
double GetFinalPrice() const {
return 0.6*EBook::GetBasePrice() + 0.4*PrintedBook::GetBasePrice();
}
void Print() const override {
BookBase::Print();
cout << "Endpreis: " << GetFinalPrice() << " €" << endl;
}
};
int main() {
HybridBook hb("Modern C++", "A. Dev",
100.0, 0.1, 50.0, 400, true);
hb.Print();
}
7. Designhinweise und Alternativen
- Mehrfachvererbung ist mächtig, aber auch komplex. Bevorzugen Sie Komposition (has-a), wenn möglich.
- Zur gemeinsamen Nutzung von Verhalten sind reine virtuelle Schnittstellen (Interfaces) oft einfacher.
- Bei virtueller Vererbung initialisiert immer die am weitesten abgeleitete Klasse die virtuelle Basis.
- Verwenden Sie override, using oder explizite Namensraumqualifizierung, um Mehrdeutigkeiten zu vermeiden.
8. TL;DR
- Diamond Problem: dieselbe Basisklasse wird zweimal geerbt → doppelte Kopien, Mehrdeutigkeit und Redundanz.
- Virtuelle Vererbung sorgt für nur eine gemeinsame Basiskopie und beseitigt die Mehrdeutigkeit.
- Der am weitesten abgeleitete Typ ruft den Konstruktor der virtuellen Basis auf.
- Lösen Sie Mehrdeutigkeit mit Qualifizierung (
A::Foo), override oder using. - Bevorzugen Sie Komposition oder abstrakte Schnittstellen anstelle von Mehrfachvererbung, wenn möglich.
- Alle Beispiele sind mit Visual Studio 2022 und GCC 11+ kompatibel.