Dziedziczenie wielokrotne pozwala jednej klasie dziedziczyć interfejs i implementację z więcej niż jednej klasy bazowej. To potężna cecha C++, która była szeroko stosowana od wczesnych dni języka do realizacji skomplikowanych architektur.
Historia problemu: Dziedziczenie wielokrotne zostało dodane do C++ jako środek tworzenia komponentów do ponownego użycia i łączenia różnych ról w jednej klasie (na przykład, gdy obiekt jest jednocześnie wątkiem i kolejką).
Problem: Główne trudności związane z dziedziczeniem wielokrotnym to 'diamond problem' (problem rombowego dziedziczenia), niejednoznaczność dostępu do członków klas bazowych oraz nieoczywisty porządek wywołania konstruktorów/destruktorów.
Rozwiązanie: Aby uniknąć wskazanych problemów, w C++ przewidziano dziedziczenie wirtualne. Gwarantuje ono, że wspólny przodek zostanie utworzony dokładnie raz, nawet jeśli przez hierarchię prowadzi do niego kilka łańcuchów, i zapewnia odpowiedni porządek inicjalizacji/zniszczenia.
Przykład kodu:
class A { public: int value; A() : value(1) {} }; class B : virtual public A {}; class C : virtual public A {}; class D : public B, public C {}; int main() { D d; d.value = 10; // OK, tylko jeden A return 0; }
Kluczowe cechy:
Jeśli klasa bazowa jest dziedziczona dwukrotnie (z lewej i z prawej), ile kopii tej klasy będzie w pamięci obiektu?
Domyślnie dwie, jeśli nie używa się dziedziczenia wirtualnego. Tylko przy dziedziczeniu wirtualnym będzie dokładnie jedna kopia.
Czy można wyraźnie wskazać, do której klasy bazowej odnosi się dostęp do członu, jeśli pojawia się niejednoznaczność?
Tak, używając kwalifikatorów:
d.B::value = 5; d.C::value = 6;
Jak określa się kolejność wywołania konstruktorów i destruktorów w przypadku dziedziczenia wielokrotnego?
Kolejność wywołania konstruktorów odpowiada kolejności deklaracji klas bazowych na liście dziedziczenia (od lewej do prawej), a następnie samej klasy pochodnej. Dla destruktorów — odwrotnie.
Programista implementuje system logowania i kolejek za pomocą dziedziczenia wielokrotnego, nie zdając sobie sprawy z diamond problem. W rezultacie wspólny logger jest inicjalizowany dwukrotnie, co prowadzi do konfliktu przy zwalnianiu zasobów.
Zalety:
Wady:
Używa się dziedziczenia wirtualnego dla wspólnego loggera, a członkowie klasy są wyraźnie inicjowani w konstruktorze.
Zalety:
Wady: