class A { public: void eat(){ cout<<"A";} };
class B: virtual public A { public: void eat(){ cout<<"B";} };
class C: virtual public A { public: void eat(){ cout<<"C";} };
class D: public B,C { public: void eat(){ cout<<"D";} };
int main(){
A *a = new D();
a->eat();
}
Rozumiem problem z diamentami, a powyższy fragment kodu nie ma tego problemu.
Jak dokładnie wirtualne dziedziczenie rozwiązuje problem?
Co rozumiem:
kiedy mówię A *a = new D();
, kompilator chce wiedzieć, czy obiekt typu D
można przypisać do wskaźnika typu A
, ale ma dwie ścieżki, którymi może podążać, ale nie może sam zdecydować.
Jak więc wirtualne dziedziczenie rozwiązuje problem (pomaga kompilatorowi w podjęciu decyzji)?
B
lubC
'? Dzięki!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 rodzicaD
), 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 jakB
iC
spróbuje utworzyć różne instancjeA
np. wywołania sparametryzowanego konstruktora z różnymi parametrami (D::D(int x, int y): C(x), B(y) {}
)? Która instancjaA
woli zostanie wybrana jako częśćD
?B
, ale wirtualnegoC
? Czy wystarczy do stworzenia jednej instancjiA
wD
?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; // error: request for member 'getX' is ambiguous //cout << "d.getX() = " << d.getX() << endl; // error: 'A' is an ambiguous base of 'D' //cout << "d.A::getX() = " << d.A::getX() << 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);
tworzyA(2)
zgodnie z oczekiwaniami, to samo dlaC 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ównoB
iC
każdy z nich tworząc swój własnyA
, więc mamy podwójneA
wd
: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; //uncommented cout << "d.A::getX() = " << d.A::getX() << endl; //uncommented ...
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ówB
iC
. Ponieważ niejednoznaczność zniknęła, wszystkie wywołaniagetX()
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 konstruktoraD
: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 klasaB
jest teraz odpowiedzialna za tworzenieA
samodzielnie, ponieważB
nie zrobi tego automatycznie.Mając to na uwadze, łatwo odpowiedzieć na wszystkie pytania, które miałem:
D
tworzenia aniB
nieC
jest odpowiedzialny za parametryA
, jest to całkowicie zależneD
tylko od.C
przekaże tworzenieA
doD
, aleB
utworzy własną instancjęA
przywracającą w ten sposób problem z diamentamiźródło
virtual
słowa kluczowego jako „zdefiniowane później (w podklasach)”, to znaczy nie zdefiniowane „naprawdę”, ale zdefiniowane „wirtualnie”. Ta interpretacja działa nie tylko dla klas bazowych, ale także dla metod. Dziękuję Ci!Wystąpienia klas pochodnych przechowują członków ich klas podstawowych.
Bez dziedziczenia wirtualnego układy pamięci wyglądają tak (zwróć uwagę na dwie kopie
A
członków w klasieD
):class A: [A members] class B: public A [A members|B members] class C: public A [A members|C members] class D: public B, public C [A members|B members|A members|C members|D members]
W przypadku dziedziczenia wirtualnego układy pamięci wyglądają następująco (zwróć uwagę na pojedynczą kopię
A
członków w klasieD
):class A: [A members] class B: virtual public A [B members|A members] | ^ v | virtual table B class C: virtual public A [C members|A members] | ^ v | virtual table C class D: public B, public C [B members|C members|D members|A members] | | ^ v v | virtual table D ----------------|
Dla każdej klasy pochodnej kompilator tworzy tabelę wirtualną zawierającą wskaźniki do elementów członkowskich jej wirtualnych klas bazowych przechowywanych w klasie pochodnej i dodaje wskaźnik do tej tabeli wirtualnej w klasie pochodnej.
źródło
Problemem nie jest ścieżka, którą musi podążać kompilator. Problemem jest punkt końcowy tej ścieżki: wynik rzutowania. Jeśli chodzi o konwersje tekstów, ścieżka nie ma znaczenia, liczy się tylko wynik końcowy.
Jeśli używasz zwykłego dziedziczenia, każda ścieżka ma swój własny charakterystyczny punkt końcowy, co oznacza, że wynik rzutowania jest niejednoznaczny, co jest problemem.
Jeśli używasz dziedziczenia wirtualnego, otrzymasz hierarchię w kształcie rombu: obie ścieżki prowadzą do tego samego punktu końcowego. W tym przypadku problem wyboru ścieżki już nie istnieje (a dokładniej już nie ma znaczenia), ponieważ obie ścieżki prowadzą do tego samego rezultatu. Rezultat nie jest już niejednoznaczny - to się liczy. Dokładna ścieżka nie.
źródło
Właściwie przykład powinien wyglądać następująco:
#include <iostream> //THE DIAMOND PROBLEM SOLVED!!! class A { public: virtual ~A(){ } virtual void eat(){ std::cout<<"EAT=>A";} }; class B: virtual public A { public: virtual ~B(){ } virtual void eat(){ std::cout<<"EAT=>B";} }; class C: virtual public A { public: virtual ~C(){ } virtual void eat(){ std::cout<<"EAT=>C";} }; class D: public B,C { public: virtual ~D(){ } virtual void eat(){ std::cout<<"EAT=>D";} }; int main(int argc, char ** argv){ A *a = new D(); a->eat(); delete a; }
... w ten sposób wynik będzie prawidłowy: „EAT => D”
Wirtualne dziedziczenie rozwiązuje tylko problem duplikacji dziadka! ALE nadal musisz określić metody, które mają być wirtualne, aby metody zostały poprawnie zastąpione ...
źródło