webentwicklung-frage-antwort-db.com.de

Wie löst die virtuelle Vererbung die Mehrdeutigkeit (Raute) (Mehrfachvererbung)?

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 über Code hat dieses Problem nicht.

Wie löst die virtuelle Vererbung das Problem genau?

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

Wie kann also das Problem durch virtuelle Vererbung gelöst werden (Hilfe beim Compiler bei der Entscheidung)?

70
Moeb

Sie wollen: (Erreichbar mit virtueller Vererbung)

  A  
 / \  
B   C  
 \ /  
  D 

And not: (Was passiert ohne virtuelle Vererbung)

A   A  
|   |
B   C  
 \ /  
  D 

Virtuelle Vererbung bedeutet, dass es nur eine Instanz der A-Basisklasse gibt, nicht 2. 

Ihr Typ D hätte 2 vtable-Zeiger (Sie können sie im ersten Diagramm sehen), einen für B und einen für C, die A praktisch erben. Die Objektgröße von D wird erhöht, da jetzt 2 Zeiger gespeichert werden. Es gibt jedoch nur eine A

B::A und C::A sind also gleich und es können keine mehrdeutigen Aufrufe von D erfolgen. Wenn Sie keine virtuelle Vererbung verwenden, haben Sie das zweite Diagramm oben. Jeder Anruf an ein Mitglied von A wird dann mehrdeutig und Sie müssen angeben, welchen Pfad Sie nehmen möchten.

Wikipedia hat einen anderen guten Überblick und ein Beispiel hier

82
Brian R. Bondy

Instanzen abgeleiteter Klassen "enthalten" Instanzen von Basisklassen, sodass sie im Arbeitsspeicher so 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 folgendermaßen 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 in abgeleiteten Klassen zur Laufzeit ein vtable-Zeiger festgelegt ist, der auf Daten der Basisklasse zeigt, 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
39
el.pescado

Warum noch eine Antwort?

Nun, viele Beiträge zu SO und Artikeln außerhalb sagen, dass das Diamantproblem gelöst wird, indem eine Einzelinstanz von A anstelle von zwei erstellt wird (eine für jedes Elternteil von D), wodurch Mehrdeutigkeiten gelöst werden. Dies hat mir jedoch kein umfassendes Verständnis für den Prozess vermittelt, ich hatte noch mehr Fragen wie

  1. was ist, wenn B und C versuchen, verschiedene Instanzen von A zu erstellen, z. Aufruf des parametrisierten Konstruktors mit verschiedenen Parametern (D::D(int x, int y): C(x), B(y) {})? Welche Instanz von A wird ausgewählt, um Teil von D zu werden?
  2. was ist, wenn ich nicht-virtuelle Vererbung für B, aber virtuelle für C verwende? Reicht es aus, um eine Einzelinstanz von A in D zu erstellen?
  3. sollte ich ab jetzt standardmäßig die virtuelle Vererbung standardmäßig als vorbeugende Maßnahme verwenden, da dadurch ein eventuelles Diamantproblem mit geringen Leistungskosten und ohne andere Nachteile gelöst wird?

Verhalten nicht vorhersagen zu können, ohne Codebeispiele auszuprobieren, bedeutet, das Konzept nicht zu verstehen. Nachfolgend finden Sie Informationen, die mir dabei geholfen haben, die virtuelle Vererbung zu umwickeln. 

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

Lasst uns durch die Ausgabe gehen. Wenn Sie B b(2); ausführen, wird A(2) wie erwartet erstellt, ebenso 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); benötigt sowohl B als auch C, wobei jeder von ihnen seine eigene A erstellt. Daher haben wir in A doppelt 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(), einen Kompilierungsfehler zu verursachen, da der Compiler nicht auswählen kann, für welche A-Instanz er die Methode aufrufen soll. Es ist immer noch möglich, Methoden direkt für die ausgewählte übergeordnete Klasse aufzurufen:

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

Virtualität

Jetzt können Sie virtuelle Vererbung hinzufügen. Verwenden Sie dasselbe Codebeispiel 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
...

Lass uns zur Erstellung von d springen:

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

Sie können sehen, dass A mit einem Standardkonstruktor erstellt wird, der Parameter ignoriert, die von Konstruktoren von B und C übergeben werden. Wenn die Mehrdeutigkeit verschwunden ist, geben alle Aufrufe an getX() denselben Wert zurück:

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

Was aber, wenn wir den parametrisierten Konstruktor für A aufrufen möchten? Dies kann durch expliziten Aufruf des Konstruktors von D erfolgen:

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

Normalerweise kann die Klasse nur Konstruktoren von direkten Eltern explizit verwenden, es gibt jedoch einen Ausschluss für den Fall der virtuellen Vererbung. Die Entdeckung dieser Regel hat für mich "geklickt" und dabei geholfen, die virtuellen Schnittstellen zu verstehen:

Code class B: virtual A bedeutet, dass jede Klasse, die von B geerbt wurde, nun selbst dafür verantwortlich ist, A zu erstellen, da B dies nicht automatisch tut.

Vor diesem Hintergrund ist es einfach, alle Fragen zu beantworten, die ich hatte:

  1. Während der Erstellung von D ist weder B noch C für die Parameter von A verantwortlich, es liegt ausschließlich an D.
  2. C wird die Erstellung von A an D delegieren, aber B erstellt eine eigene Instanz von A, wodurch das Diamantenproblem zurückgebracht wird
  3. Das Definieren von Basisklassenparametern in der Enkelklasse anstelle eines direkten Kindes ist nicht empfehlenswert. Daher sollte dies toleriert werden, wenn ein Diamantproblem vorliegt und diese Maßnahme nicht zu vermeiden ist.
10
nnovich-OK

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

Wenn Sie die normale Vererbung verwenden, hat jeder Pfad seinen eigenen Endpunkt. Dies bedeutet, dass das Ergebnis der Umwandlung mehrdeutig ist. Dies ist das Problem.

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 Wahl des Pfads nicht mehr (oder genauer ist es nicht mehr von Bedeutung), da beide Pfade zum gleichen Ergebnis führen. Das Ergebnis ist nicht mehr zweideutig - darauf kommt es an. Der genaue Pfad stimmt nicht.

8
AnT

Eigentlich sollte das Beispiel wie folgt aussehen:

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

... so wird die Ausgabe die richtige sein: "EAT => D"

Bei der virtuellen Vererbung wird nur die Duplizierung des Großvaters gelöst.

7
enger

Das richtige Codebeispiel ist hier. Das Diamantproblem:

#include <iostream>
// Here you have the diamond problem : there is B::eat() and C::eat()
// because they both inherit from A and contain independent copies of A::eat()
// So what is D::eat()? Is it B::eat() or C::eat() ?
class A { public: void eat(){ std::cout << "CHROME-CHROME" << endl; } };
class B: public A   { };
class C: public A   { };
class D: public B,C { };

int main(int argc, char ** argv){
    A *a = new D(); 
    a->eat(); 
    delete a;
}

Die Lösung :

#include <iostream>
// Virtual inheritance to ensure B::eat() and C::eat() to be the same 
class A { public: void eat(){ std::cout<< "CHROME-CHROME" << endl; } };
class B: virtual public A   { };
class C: virtual public A   { };
class D: public         B,C { };

int main(int argc, char ** argv){
    A *a = new D(); 
    a->eat(); 
    delete a;
}
0
tkrishtop