W jaki sposób dziedziczenie wirtualne rozwiązuje niejednoznaczność „diamentu” (dziedziczenie wielokrotne)?

97
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 Dmoż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)?

Moeb
źródło

Odpowiedzi:

112

Chcesz: (Osiągalne dzięki dziedziczeniu wirtualnemu)

  A  
 / \  
B   C  
 \ /  
  D 

I nie: (Co dzieje się bez wirtualnego dziedziczenia)

A   A  
|   |
B   C  
 \ /  
  D 

Dziedziczenie wirtualne oznacza, że ​​będzie tylko 1 wystąpienie Aklasy bazowej, a nie 2.

Twój typ Dmiałby 2 wskaźniki vtable (możesz je zobaczyć na pierwszym diagramie), jeden dla, Ba drugi dla tego, Ckto praktycznie dziedziczy A. DRozmiar obiektu jest zwiększony, ponieważ przechowuje teraz 2 wskaźniki; jednak teraz jest tylko jeden A.

Więc B::Ai C::Asą takie same, więc nie może być niejednoznacznych wywołań z D. Jeśli nie używasz dziedziczenia wirtualnego, masz drugi diagram powyżej. Każde wezwanie do członka A staje się wtedy niejednoznaczne i musisz określić, którą ścieżkę chcesz podążać.

Wikipedia ma kolejny dobry przegląd i przykład tutaj

Brian R. Bondy
źródło
2
Wskaźnik Vtable to szczegół implementacji. Nie wszystkie kompilatory wprowadzą w tym przypadku wskaźniki vtable.
ciekawy facet
19
Myślę, że lepiej by wyglądało, gdyby wykresy były odwzorowywane w pionie. W większości przypadków znalazłem takie diagramy dziedziczenia, które pokazują pochodne klasy poniżej podstaw. (patrz „downcast”, „
upcast
Jak mogę zmodyfikować jego kod, aby zamiast tego używał implementacji ' Blub C'? Dzięki!
Minh Nghĩa
46

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ę Azamiast 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

  1. co jeśli Bi Cspróbuje utworzyć różne instancje Anp. wywołania sparametryzowanego konstruktora z różnymi parametrami ( D::D(int x, int y): C(x), B(y) {})? Która instancja Awoli zostanie wybrana jako część D?
  2. co się stanie, jeśli użyję dziedziczenia niewirtualnego B, ale wirtualnego C? Czy wystarczy do stworzenia jednej instancji Aw D?
  3. 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;

    // 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);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 Bi Ckażdy z nich tworząc swój własny A, więc mamy podwójne Aw 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ć Ainstancji, 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ć, Ajest tworzony z domyślnym konstruktorem ignorującym parametry przekazane z konstruktorów Bi 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 Aoznacza, że ​​każda dziedziczona klasa Bjest teraz odpowiedzialna za tworzenie Asamodzielnie, ponieważ Bnie zrobi tego automatycznie.

Mając to na uwadze, łatwo odpowiedzieć na wszystkie pytania, które miałem:

  1. Podczas Dtworzenia ani Bnie Cjest odpowiedzialny za parametry A, jest to całkowicie zależne Dtylko od.
  2. Cprzekaże tworzenie Ado D, ale Butworzy własną instancję Aprzywracającą w ten sposób problem z diamentami
  3. 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.
nnovich-OK
źródło
Ta odpowiedź jest niezwykle pouczająca! Zwłaszcza twoja interpretacja virtualsł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!
Maggyero
45

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 Aczłonków w klasie D):

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ę Aczłonków w klasie D):

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.

el.pescado
źródło
10

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.

Mrówka
źródło
@Andrey: W jaki sposób kompilator implementuje dziedziczenie ... Mam na myśli twój argument i chcę ci podziękować za wyjaśnienie go w tak przejrzysty sposób ... ale naprawdę pomogłoby, gdybyś mógł wyjaśnić (lub wskazać odniesienie) co do jak kompilator faktycznie implementuje dziedziczenie i co się zmienia, kiedy robię dziedziczenie wirtualne
Bruce
8

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 ...

enger
źródło