Wie löst die virtuelle Vererbung die Mehrdeutigkeit des „Diamanten“ (Mehrfachvererbung)?

95
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(); 
} 

Ich verstehe das Diamantproblem, und der obige Code hat dieses Problem nicht.

Wie genau löst die virtuelle Vererbung das Problem?

Was ich verstehe: Wenn ich sage A *a = new D();, möchte der Compiler wissen, ob ein Objekt vom Typ Deinem Zeiger vom Typ zugewiesen werden kann A, aber es hat zwei Pfade, denen er folgen kann, aber nicht selbst entscheiden kann.

Wie löst die virtuelle Vererbung das Problem (helfen Sie dem Compiler, die Entscheidung zu treffen)?

Moeb
quelle

Antworten:

108

Sie möchten: (Erreichbar mit virtueller Vererbung)

  A  
 / \  
B   C  
 \ /  
  D 

Und nicht: (Was passiert ohne virtuelle Vererbung)

A   A  
|   |
B   C  
 \ /  
  D 

Virtuelle Vererbung bedeutet, dass nur eine Instanz der Basisklasse vorhanden ist A, nicht 2.

Ihr Typ Dwürde 2 vtable-Zeiger haben (Sie können sie im ersten Diagramm sehen), einen für Bund einen für diejenigen, Cdie virtuell erben A. DDie Objektgröße wird erhöht, da jetzt 2 Zeiger gespeichert werden. Ajetzt gibt es jedoch nur einen .

So B::Aund C::Asind gleich und so kann es keine mehrdeutigen Anrufe von geben D. Wenn Sie keine virtuelle Vererbung verwenden, sehen Sie das zweite Diagramm oben. Und jeder Aufruf eines Mitglieds von A wird dann mehrdeutig und Sie müssen angeben, welchen Pfad Sie einschlagen möchten.

Wikipedia hat hier einen weiteren guten Überblick und ein gutes Beispiel

Brian R. Bondy
quelle
2
Der Vtable-Zeiger ist ein Implementierungsdetail. In diesem Fall führen nicht alle Compiler vtable-Zeiger ein.
Neugieriger
19
Ich denke, es würde besser aussehen, wenn die Grafiken vertikal gespiegelt würden. In den meisten Fällen habe ich solche Vererbungsdiagramme gefunden, um die abgeleiteten Klassen unterhalb der Basen anzuzeigen. (siehe "downcast", "upcast")
peterh - Reinstate Monica
Wie kann ich seinen Code ändern , um stattdessen die Implementierung von B'oder C' zu verwenden? Vielen Dank!
Minh Nghĩa
44

Instanzen abgeleiteter Klassen "enthalten" Instanzen von Basisklassen, sodass sie im Speicher folgendermaßen aussehen:

class A: [A fields]
class B: [A fields | B fields]
class C: [A fields | C fields]

Ohne virtuelle Vererbung würde die Instanz der Klasse D also wie folgt aussehen:

class D: [A fields | B fields | A fields | C fields | D fields]
          '- derived from B -' '- derived from C -'

Beachten Sie also zwei "Kopien" von A-Daten. Virtuelle Vererbung bedeutet, dass innerhalb der abgeleiteten Klasse zur Laufzeit ein vtable-Zeiger festgelegt ist, der auf Daten der Basisklasse verweist, sodass Instanzen von B-, C- und D-Klassen folgendermaßen aussehen:

class B: [A fields | B fields]
          ^---------- pointer to A

class C: [A fields | C fields]
          ^---------- pointer to A

class D: [A fields | B fields | C fields | D fields]
          ^---------- pointer to B::A
          ^--------------------- pointer to C::A
el.pescado
quelle
43

Warum noch eine Antwort?

Nun, viele Posts auf SO und Artikel außerhalb sagen, dass das Diamantproblem gelöst wird, indem eine einzelne Instanz Aanstelle von zwei erstellt wird (eine für jedes Elternteil von D), wodurch Mehrdeutigkeiten aufgelöst werden. Dies gab mir jedoch kein umfassendes Verständnis des Prozesses, und ich bekam noch mehr Fragen wie

  1. Was ist, wenn Bund Cversucht, verschiedene Instanzen von Az. B. dem Aufrufen eines parametrisierten Konstruktors mit verschiedenen Parametern ( D::D(int x, int y): C(x), B(y) {}) zu erstellen ? Welche Instanz von Awird ausgewählt, um Teil davon zu werden D?
  2. Was ist, wenn ich nicht-virtuelle Vererbung für B, aber virtuelle Vererbung für verwende C? Reicht es aus, eine einzelne Instanz von Ain zu erstellen D?
  3. Sollte ich von nun an standardmäßig immer die virtuelle Vererbung als vorbeugende Maßnahme verwenden, da sie mögliche Diamantprobleme mit geringen Leistungskosten und ohne weitere Nachteile löst?

Das Verhalten nicht vorhersagen zu können, ohne Codebeispiele auszuprobieren, bedeutet, das Konzept nicht zu verstehen. Das Folgende hat mir geholfen, mich mit der virtuellen Vererbung zu befassen.

Doppel a

Beginnen wir zunächst mit diesem Code ohne virtuelle Vererbung:

#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;
}

Gehen wir die Ausgabe durch. Das Ausführen B b(2);erstellt A(2)wie erwartet, dasselbe für 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);braucht beide Bund C, jeder von ihnen schafft seine eigenen A, so haben wir doppelt Ain d:

Create d(2,3): 
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3) 

Dies ist der Grund für d.getX()den Kompilierungsfehler, da der Compiler nicht auswählen kann, für welche AInstanz er die Methode aufrufen soll. Es ist jedoch möglich, Methoden direkt für die ausgewählte übergeordnete Klasse aufzurufen:

d.B::getX() = 3
d.C::getX() = 2

Virtualität

Fügen wir nun die virtuelle Vererbung hinzu. Verwenden des gleichen Codebeispiels mit den folgenden Änderungen:

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

Springen wir zur Erstellung von d:

Create d(2,3): 
A::A() C::C(2) B::B(3) D::D(2, 3) 

Sie können sehen, Awird mit dem Standardkonstruktor erstellt, der die von den Konstruktoren von Bund übergebenen Parameter ignoriert C. Da die Mehrdeutigkeit weg ist, geben alle Aufrufe getX()denselben Wert zurück:

d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42

Aber was ist, wenn wir einen parametrisierten Konstruktor aufrufen wollen A? Dies kann durch expliziten Aufruf vom Konstruktor von D:

D(int x, int y, int z): A(x), C(y), B(z)

Normalerweise kann die Klasse explizit nur Konstruktoren direkter Eltern verwenden, es gibt jedoch einen Ausschluss für den Fall der virtuellen Vererbung. Das Entdecken dieser Regel hat für mich "geklickt" und mir geholfen, virtuelle Schnittstellen zu verstehen:

Code class B: virtual Abedeutet, dass jede Klasse, von der geerbt wurde, Bjetzt selbst erstellt Awerden muss, da dies Bnicht automatisch erfolgt.

Mit dieser Aussage ist es einfach, alle Fragen zu beantworten, die ich hatte:

  1. Während der DErstellung ist weder für Bnoch Cverantwortlich für Parameter von A, es liegt ganz bei Dnur.
  2. Cwird die Erstellung von Aan delegieren D, aber Beine eigene Instanz erstellen A, um das Diamantproblem zurückzubringen
  3. Das Definieren von Basisklassenparametern in der Enkelklasse anstelle eines direkten Kindes ist keine gute Vorgehensweise. Daher sollte dies toleriert werden, wenn ein Diamantproblem vorliegt und diese Maßnahme unvermeidbar ist.
nnovich-OK
quelle
10

Das Problem ist nicht der Pfad, dem der Compiler folgen muss. Das Problem ist der Endpunkt dieses Pfades: das Ergebnis der Besetzung. Bei Typkonvertierungen spielt der Pfad keine Rolle, nur das Endergebnis.

Wenn Sie die normale Vererbung verwenden, hat jeder Pfad einen eigenen Endpunkt, was bedeutet, dass das Ergebnis der Umwandlung nicht eindeutig ist, was das Problem ist.

Wenn Sie die virtuelle Vererbung verwenden, erhalten Sie eine rautenförmige Hierarchie: Beide Pfade führen zum gleichen Endpunkt. In diesem Fall besteht das Problem der Pfadauswahl nicht mehr (oder genauer gesagt spielt es keine Rolle mehr), da beide Pfade zum gleichen Ergebnis führen. Das Ergebnis ist nicht mehr mehrdeutig - darauf kommt es an. Der genaue Pfad stimmt nicht.

Ameise
quelle
@Andrey: Wie implementiert der Compiler die Vererbung? Ich meine, ich verstehe Ihr Argument und möchte Ihnen dafür danken, dass Sie es so klar erklärt haben. Aber es wäre wirklich hilfreich, wenn Sie erklären (oder auf eine Referenz verweisen) könnten Wie der Compiler die Vererbung tatsächlich implementiert und was sich ändert, wenn ich die virtuelle Vererbung durchführe
Bruce
8

Eigentlich sollte das Beispiel wie folgt sein:

#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;
}

... auf diese Weise wird die Ausgabe die richtige sein: "EAT => D"

Virtuelle Vererbung löst nur die Vervielfältigung des Großvaters! ABER Sie müssen immer noch die Methoden angeben, die virtuell sein sollen, damit die Methoden korrekt überschrieben werden ...

enger
quelle