Dlaczego inna odpowiedź?
Cóż, wiele postów na SO i artykuły na zewnątrz mówi, że problem z diamentami można rozwiązać, tworząc jedną instancję A
zamiast dwóch (po jednej dla każdego rodzica D
), rozwiązując w ten sposób niejednoznaczność. Jednak nie dało mi to pełnego zrozumienia procesu, skończyło się na jeszcze większej liczbie pytań, takich jak
- co jeśli
B
i C
spróbuje utworzyć różne instancje A
np. wywołania sparametryzowanego konstruktora z różnymi parametrami ( D::D(int x, int y): C(x), B(y) {}
)? Która instancja A
woli zostanie wybrana jako część D
?
- co się stanie, jeśli użyję dziedziczenia niewirtualnego
B
, ale wirtualnego C
? Czy wystarczy do stworzenia jednej instancji A
w D
?
- czy powinienem zawsze domyślnie używać dziedziczenia wirtualnego jako środka zapobiegawczego, ponieważ rozwiązuje ono możliwy problem z diamentami przy niewielkich kosztach wydajności i bez innych wad?
Brak możliwości przewidzenia zachowania bez wypróbowania próbek kodu oznacza niezrozumienie koncepcji. Oto, co pomogło mi w omówieniu dziedziczenia wirtualnego.
Podwójne A
Po pierwsze, zacznijmy od tego kodu bez wirtualnego dziedziczenia:
#include<iostream>
using namespace std;
class A {
public:
A() { cout << "A::A() "; }
A(int x) : m_x(x) { cout << "A::A(" << x << ") "; }
int getX() const { return m_x; }
private:
int m_x = 42;
};
class B : public A {
public:
B(int x):A(x) { cout << "B::B(" << x << ") "; }
};
class C : public A {
public:
C(int x):A(x) { cout << "C::C(" << x << ") "; }
};
class D : public C, public B {
public:
D(int x, int y): C(x), B(y) {
cout << "D::D(" << x << ", " << y << ") "; }
};
int main() {
cout << "Create b(2): " << endl;
B b(2); cout << endl << endl;
cout << "Create c(3): " << endl;
C c(3); cout << endl << endl;
cout << "Create d(2,3): " << endl;
D d(2, 3); cout << endl << endl;
cout << "d.B::getX() = " << d.B::getX() << endl;
cout << "d.C::getX() = " << d.C::getX() << endl;
}
Przejdźmy przez wyjście. Wykonywanie B b(2);
tworzy A(2)
zgodnie z oczekiwaniami, to samo dla C c(3);
:
Create b(2):
A::A(2) B::B(2)
Create c(3):
A::A(3) C::C(3)
D d(2, 3);
potrzebuje zarówno B
i C
każdy z nich tworząc swój własny A
, więc mamy podwójne A
w d
:
Create d(2,3):
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3)
Z tego powodu d.getX()
powoduje błąd kompilacji, ponieważ kompilator nie może wybrać A
instancji, dla której ma wywołać metodę. Nadal możliwe jest wywołanie metod bezpośrednio dla wybranej klasy nadrzędnej:
d.B::getX() = 3
d.C::getX() = 2
Wirtualność
Teraz dodajmy dziedziczenie wirtualne. Korzystanie z tego samego przykładu kodu z następującymi zmianami:
class B : virtual public A
...
class C : virtual public A
...
cout << "d.getX() = " << d.getX() << endl;
cout << "d.A::getX() = " << d.A::getX() << endl;
...
Przejdźmy do tworzenia d
:
Create d(2,3):
A::A() C::C(2) B::B(3) D::D(2, 3)
Jak widać, A
jest tworzony z domyślnym konstruktorem ignorującym parametry przekazane z konstruktorów B
i C
. Ponieważ niejednoznaczność zniknęła, wszystkie wywołania getX()
zwracające tę samą wartość:
d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42
Ale co, jeśli chcemy wywołać sparametryzowany konstruktor dla A
? Można to zrobić poprzez jawne wywołanie go z konstruktora D
:
D(int x, int y, int z): A(x), C(y), B(z)
Zwykle klasa może jawnie używać konstruktorów tylko bezpośrednich rodziców, ale istnieje wykluczenie dla przypadku dziedziczenia wirtualnego. Odkrycie tej reguły "kliknęło" dla mnie i bardzo pomogło w zrozumieniu wirtualnych interfejsów:
Kod class B: virtual A
oznacza, że każda dziedziczona klasa B
jest teraz odpowiedzialna za tworzenie A
samodzielnie, ponieważ B
nie zrobi tego automatycznie.
Mając to na uwadze, łatwo odpowiedzieć na wszystkie pytania, które miałem:
- Podczas
D
tworzenia ani B
nie C
jest odpowiedzialny za parametry A
, jest to całkowicie zależne D
tylko od.
C
przekaże tworzenie A
do D
, ale B
utworzy własną instancję A
przywracającą w ten sposób problem z diamentami
- Definiowanie parametrów klasy bazowej w klasie wnuczka, a nie bezpośredniego dziecka, nie jest dobrą praktyką, więc powinno być tolerowane, gdy istnieje problem z diamentami i jest to nieuniknione.