Wie löst die virtuelle Vererbung die “Diamant”-Mehrdeutigkeit (Mehrfachvererbung)?

Lesezeit: 10 Minuten

Wie lost die virtuelle Vererbung die Diamant Mehrdeutigkeit Mehrfachvererbung
Möb

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 D kann einem Zeiger vom Typ zugewiesen werden Aaber es hat zwei Wege, denen es folgen kann, die es aber nicht selbst entscheiden kann.

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

Wie lost die virtuelle Vererbung die Diamant Mehrdeutigkeit Mehrfachvererbung
Brian R. Bondy

Sie wollen: (Erreichbar mit virtueller Vererbung)

  A  
 / \  
B   C  
 \ /  
  D 

Und nicht: (Was passiert ohne virtuelle Vererbung)

A   A  
|   |
B   C  
 \ /  
  D 

Virtuelle Vererbung bedeutet, dass es nur eine Instanz der Basis gibt A Klasse nicht 2.

Dein Typ D hätte 2 vtable-Zeiger (Sie können sie im ersten Diagramm sehen), einen für B und eine für C die quasi erben A. DDie Objektgröße von wurde erhöht, weil es jetzt 2 Zeiger speichert; jedoch gibt es nur einen A jetzt.

Damit B::A und C::A sind gleich und so kann es zu keinen mehrdeutigen Aufrufen kommen D. Wenn Sie keine virtuelle Vererbung verwenden, haben Sie das zweite Diagramm oben. Und jeder Aufruf an ein Mitglied von A wird dann mehrdeutig und Sie müssen angeben, welchen Weg Sie einschlagen möchten.

Wikipedia hat hier einen weiteren guten Überblick und ein Beispiel

  • Der Vtable-Zeiger ist ein Implementierungsdetail. Nicht alle Compiler führen in diesem Fall vtable-Zeiger ein.

    – Neugieriger

    23. Juni 2016 um 1:56 Uhr

  • Ich denke, es würde besser aussehen, wenn die Diagramme vertikal gespiegelt würden. In den meisten Fällen habe ich solche Vererbungsdiagramme gefunden, um die abgeleiteten Klassen unterhalb der Basen anzuzeigen. (siehe „Niedergeschlagen“, „Aufwärts gerichtet“)

    – Peterh

    8. Juli 2016 um 20:27 Uhr


  • Wie kann ich seinen Code ändern, um ihn zu verwenden? B‘s oder C‘s-Implementierung stattdessen? Danke!

    – Minh Nghĩa

    15. Juli 2019 um 3:31 Uhr

Warum noch eine Antwort?

Nun, viele Posts auf SO und Artikel außerhalb sagen, dass das Diamond-Problem gelöst wird, indem eine einzelne Instanz von erstellt wird A statt zwei (eins für jeden Elternteil von D), wodurch Mehrdeutigkeiten beseitigt werden. Dies gab mir jedoch kein umfassendes Verständnis des Prozesses, ich endete mit noch mehr Fragen wie

  1. was, wenn B und C versucht, verschiedene Instanzen von zu erstellen A zB Aufruf parametrisierter Konstruktoren mit unterschiedlichen Parametern (D::D(int x, int y): C(x), B(y) {})? Welche Instanz von A ausgewählt werden, um Teil davon zu werden D?
  2. Was ist, wenn ich die nicht-virtuelle Vererbung verwende? Bsondern virtuell für C? Reicht es aus, um eine einzelne Instanz von zu erstellen A in D?
  3. sollte ich die virtuelle Vererbung von nun an standardmäßig als vorbeugende Maßnahme verwenden, da sie ein mögliches Diamantproblem mit geringen Leistungskosten und ohne andere Nachteile löst?

Verhalten nicht vorhersagen zu können, ohne Codebeispiele auszuprobieren, bedeutet, das Konzept nicht zu verstehen. Unten ist, was mir geholfen hat, mich um die virtuelle Vererbung zu kümmern.

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

Lassen Sie uns die Ausgabe durchgehen. Ausführen B b(2); schafft A(2) wie erwartet, das gleiche 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 beides B und Cjeder von ihnen erstellt seine eigenen Aalso haben wir doppelt A in d:

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

Das ist der Grund für d.getX() um einen Kompilierungsfehler zu verursachen, da der Compiler nicht auswählen kann, welcher A Instanz, für die die Methode aufgerufen werden soll. Dennoch ist es möglich, Methoden direkt für die ausgewählte Elternklasse aufzurufen:

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

Virtualität

Lassen Sie uns nun die virtuelle Vererbung hinzufügen. 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) 

Du kannst sehen, A wird mit dem Standardkonstruktor erstellt, wobei Parameter ignoriert werden, die von Konstruktoren von übergeben werden B und C. Da die Mehrdeutigkeit weg ist, werden alle Anrufe an getX() denselben Wert zurückgeben:

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

Aber was ist, wenn wir den parametrisierten Konstruktor for aufrufen möchten? A? Dies kann durch expliziten Aufruf vom Konstruktor von erfolgen D:

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

Normalerweise kann die Klasse explizit nur Konstruktoren direkter Eltern verwenden, aber es gibt einen Ausschluss für den Fall der virtuellen Vererbung. Die Entdeckung dieser Regel hat für mich “geklickt” und sehr geholfen, virtuelle Schnittstellen zu verstehen:

Code class B: virtual A bedeutet, dass jede Klasse geerbt wird B ist jetzt für die Erstellung verantwortlich A von selbst, da B wird es nicht automatisch tun.

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

  1. Während D Schöpfung auch nicht B Noch C ist verantwortlich für die Parameter von Aes ist ganz bis zu D nur.
  2. C wird die Erstellung von delegieren A zu Daber B erstellt eine eigene Instanz von A wodurch das Diamantproblem zurückgebracht wird
  3. Das Definieren von Basisklassenparametern in der Enkelklasse statt in der direkten Kindklasse ist keine gute Praxis, daher sollte es toleriert werden, wenn ein Rautenproblem besteht und diese Maßnahme unvermeidlich ist.

  • Diese Antwort ist sehr informativ! Vor allem Ihre Interpretation der virtual Schlüsselwort als „später definiert (in Unterklassen)“, also nicht „wirklich“ definiert, sondern „virtuell“ definiert. Diese Interpretation funktioniert nicht nur für Basisklassen, sondern auch für Methoden. Danke!

    – Maggyero

    7. September 2020 um 7:19 Uhr


  • Eine Schwäche des virtual-Beispiel (nicht von der Erklärung) scheint mir, dass es zwar ausreichen sollte, das zu haben A-ctor anrufen D-ctors Init-Liste, es muss auch in angegeben werden B,C-ctor Init listet auch auf. Wenn Sie es dort draußen lassen (und die 0-Argumente ctor A() was in diesem Fall implizit angenommen wird) wird es nicht kompilieren. Diese Notwendigkeit erscheint mir überflüssig. In komplexeren Fällen erfordert dies die Definition bedeutungsloser 0-Argument-Ktoren A() oder bedeutungslos schreiben A(..)-ctor-ruft an B,Cs Init-Listen, während die Idee der Struktur darin bestehen sollte, diese Informationen zu verschieben D.

    – Flunk

    9. April 2021 um 9:25 Uhr

  • Beachten Sie, dass sich mein Kommentar auf das Beispiel bezieht D(int x, int y, int z): A(x), C(y), B(z). Hier A wird initialisiert als A(x) während gemäß den Init-Listen von B oder C es wäre initialisiert als A(y) oder A(z)was eigentlich nie vorkommt und somit für den Leser irreführend ist.

    – Flunk

    9. April 2021 um 9:31 Uhr


1646975410 871 Wie lost die virtuelle Vererbung die Diamant Mehrdeutigkeit Mehrfachvererbung
el.pescado – нет войне

Instanzen abgeleiteter Klassen speichern die Mitglieder ihrer Grundklassen.

Ohne virtuelle Vererbung, die Speicherlayouts aussehen (beachten Sie die zwei Kopien der A Mitglieder in der Klasse 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]

Mit virtueller Vererbung die Speicherlayouts aussehen (beachten Sie die Einzel Kopie der A Mitglieder in der Klasse 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 ----------------|

Für jede abgeleitete Klasse erstellt der Compiler eine virtuelle Tabelle, die Zeiger auf die Mitglieder ihrer virtuellen Basisklassen enthält, die in der abgeleiteten Klasse gespeichert sind, und fügt einen Zeiger zu dieser virtuellen Tabelle in der abgeleiteten Klasse hinzu.

  • @Balu: Kompilierungszeit stackoverflow.com/questions/3849498/when-is-vtable-in-c-created

    – Rasmi Ranjan Nayak

    12. Juli 2018 um 14:26 Uhr

  • Der Schaltplan ist wirklich großartig, aber ich hatte Schwierigkeiten, den letzten Satz zu verstehen, es klingt, als würde er sich nach dem letzten Komma wiederholen, nicht wahr?

    – CharMstr

    13. Dezember 2020 um 9:44 Uhr

1646975410 634 Wie lost die virtuelle Vererbung die Diamant Mehrdeutigkeit Mehrfachvererbung
Ameise

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

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

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

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”

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

  • Hier ist ein zusätzliches Beispiel, um einen Fall zu veranschaulichen, in dem (1) A eine Konstruktorvariable hat und (2) D erweitert wird: godbolt.org/z/53aKMfMar

    – D.Deriso

    9. Juli 2021 um 22:33 Uhr

  • Hier ist ein zusätzliches Beispiel, um einen Fall zu veranschaulichen, in dem (1) A eine Konstruktorvariable hat und (2) D erweitert wird: godbolt.org/z/53aKMfMar

    – D.Deriso

    9. Juli 2021 um 22:33 Uhr

989940cookie-checkWie löst die virtuelle Vererbung die “Diamant”-Mehrdeutigkeit (Mehrfachvererbung)?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy