474 Stimmen

Was ist in C++ eine virtuelle Basisklasse?

Ich möchte wissen, was ein " virtuelle Basisklasse " ist und was es bedeutet.

Lassen Sie mich ein Beispiel nennen:

class Foo
{
public:
    void DoSomething() { /* ... */ }
};

class Bar : public virtual Foo
{
public:
    void DoSpecific() { /* ... */ }
};

6voto

Luc Hermitte Punkte 30868

Zusätzlich zu dem, was bereits über Mehrfachvererbung und virtuelle Vererbung(en) gesagt wurde, gibt es einen sehr interessanten Artikel in Dr. Dobb's Journal: Mehrfachvererbung als sinnvoll erachtet

3voto

Diamond inheritance runnable Anwendungsbeispiel

Dieses Beispiel zeigt, wie eine virtuelle Basisklasse in einem typischen Szenario verwendet werden kann: um Probleme der Diamantenvererbung zu lösen.

Betrachten Sie das folgende Arbeitsbeispiel:

main.cpp

#include <cassert>

class A {
    public:
        A(){}
        A(int i) : i(i) {}
        int i;
        virtual int f() = 0;
        virtual int g() = 0;
        virtual int h() = 0;
};

class B : public virtual A {
    public:
        B(int j) : j(j) {}
        int j;
        virtual int f() { return this->i + this->j; }
};

class C : public virtual A {
    public:
        C(int k) : k(k) {}
        int k;
        virtual int g() { return this->i + this->k; }
};

class D : public B, public C {
    public:
        D(int i, int j, int k) : A(i), B(j), C(k) {}
        virtual int h() { return this->i + this->j + this->k; }
};

int main() {
    D d = D(1, 2, 4);
    assert(d.f() == 3);
    assert(d.g() == 5);
    assert(d.h() == 7);
}

Kompilieren und ausführen:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

Wenn wir die virtual in:

class B : public virtual A

würden wir eine Reihe von Fehlern erhalten, weil der GCC nicht in der Lage ist, D-Mitglieder und -Methoden aufzulösen, die zweimal über A vererbt wurden:

main.cpp:27:7: warning: virtual base ‘A’ inaccessible in ‘D’ due to ambiguity [-Wextra]
   27 | class D : public B, public C {
      |       ^
main.cpp: In member function ‘virtual int D::h()’:
main.cpp:30:40: error: request for member ‘i’ is ambiguous
   30 |         virtual int h() { return this->i + this->j + this->k; }
      |                                        ^
main.cpp:7:13: note: candidates are: ‘int A::i’
    7 |         int i;
      |             ^
main.cpp:7:13: note:                 ‘int A::i’
main.cpp: In function ‘int main()’:
main.cpp:34:20: error: invalid cast to abstract class type ‘D’
   34 |     D d = D(1, 2, 4);
      |                    ^
main.cpp:27:7: note:   because the following virtual functions are pure within ‘D’:
   27 | class D : public B, public C {
      |       ^
main.cpp:8:21: note:    ‘virtual int A::f()’
    8 |         virtual int f() = 0;
      |                     ^
main.cpp:9:21: note:    ‘virtual int A::g()’
    9 |         virtual int g() = 0;
      |                     ^
main.cpp:34:7: error: cannot declare variable ‘d’ to be of abstract type ‘D’
   34 |     D d = D(1, 2, 4);
      |       ^
In file included from /usr/include/c++/9/cassert:44,
                 from main.cpp:1:
main.cpp:35:14: error: request for member ‘f’ is ambiguous
   35 |     assert(d.f() == 3);
      |              ^
main.cpp:8:21: note: candidates are: ‘virtual int A::f()’
    8 |         virtual int f() = 0;
      |                     ^
main.cpp:17:21: note:                 ‘virtual int B::f()’
   17 |         virtual int f() { return this->i + this->j; }
      |                     ^
In file included from /usr/include/c++/9/cassert:44,
                 from main.cpp:1:
main.cpp:36:14: error: request for member ‘g’ is ambiguous
   36 |     assert(d.g() == 5);
      |              ^
main.cpp:9:21: note: candidates are: ‘virtual int A::g()’
    9 |         virtual int g() = 0;
      |                     ^
main.cpp:24:21: note:                 ‘virtual int C::g()’
   24 |         virtual int g() { return this->i + this->k; }
      |                     ^
main.cpp:9:21: note:                 ‘virtual int A::g()’
    9 |         virtual int g() = 0;
      |                     ^
./main.out

Getestet auf GCC 9.3.0, Ubuntu 20.04.

3voto

Lewis Kelsey Punkte 3199

Regelmäßige Vererbung

Bei der typischen 3-stufigen nicht-diamantenen nicht-virtuellen Vererbung wird beim Instanziieren eines neuen most-derived-object, new wird aufgerufen und die für das Objekt auf dem Heap erforderliche Größe wird vom Compiler aus dem Klassentyp aufgelöst und an new übergeben.

new hat eine Signatur:

_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)

Und macht einen Aufruf an malloc und gibt den Zeiger auf die Leere zurück

Diese Adresse wird dann an den Konstruktor des am weitesten abgeleiteten Objekts übergeben, der sofort den mittleren Konstruktor aufruft, und der mittlere Konstruktor ruft dann sofort den Basiskonstruktor auf. Der Basiskonstruktor speichert dann einen Zeiger auf seine virtuelle Tabelle am Anfang des Objekts und dann seine Attribute danach. Dieser kehrt dann zum mittleren Konstruktor zurück, der seinen Zeiger auf die virtuelle Tabelle an derselben Stelle speichert und dann seine Attribute nach den Attributen, die vom Basiskonstruktor gespeichert worden wären. Dieser kehrt dann zum am weitesten abgeleiteten Konstruktor zurück, der einen Zeiger auf seine virtuelle Tabelle an derselben Stelle speichert und dann seine Attribute nach den Attributen speichert, die vom mittleren Konstruktor gespeichert worden wären.

Da der Zeiger auf die virtuelle Tabelle überschrieben wird, ist der Zeiger auf die virtuelle Tabelle am Ende immer der Zeiger der am meisten abgeleiteten Klasse. Wenn also eine Funktion in der mittleren Klasse virtuell ist, ist sie auch in der am weitesten abgeleiteten Klasse virtuell, aber nicht in der Basisklasse. Wenn Sie eine Instanz der am weitesten abgeleiteten Klasse polymorph in einen Zeiger auf die Basisklasse umwandeln, wird der Compiler dies nicht in einen indirekten Aufruf der virtuellen Tabelle auflösen und stattdessen die Funktion direkt aufrufen A::function() . Wenn eine Funktion für den Typ, in den sie gecastet wurde, virtuell ist, führt sie zu einem Aufruf der virtuellen Tabelle, die immer diejenige der am meisten abgeleiteten Klasse ist. Wenn sie für diesen Typ nicht virtuell ist, dann wird sie einfach Type::function() und übergeben Sie ihm den Objektzeiger, der in Type umgewandelt wird.

Wenn ich von einem Zeiger auf die virtuelle Tabelle spreche, handelt es sich eigentlich immer um einen Offset von 16 in der virtuellen Tabelle.

vtable for Base:
        .quad   0
        .quad   typeinfo for Base
        .quad   Base::CommonFunction()
        .quad   Base::VirtualFunction()

pointer is typically to the first function i.e. 

        mov     edx, OFFSET FLAT:vtable for Base+16

virtual wird in weiter abgeleiteten Klassen nicht mehr benötigt, wenn es in einer weniger abgeleiteten Klasse virtuell ist, da es sich in Richtung der am meisten abgeleiteten Klasse nach unten fortpflanzt. Sie kann jedoch verwendet werden, um zu zeigen, dass die Funktion tatsächlich eine virtuelle Funktion ist, ohne dass die Typdefinitionen der Klassen, die sie erbt, überprüft werden müssen. Wenn eine Funktion als virtuell deklariert wird, wird ab diesem Zeitpunkt nur noch die letzte Implementierung in der Vererbungskette verwendet. Davor kann sie jedoch immer noch nicht-virtuell verwendet werden, wenn das Objekt in einen Typ einer Klasse gecastet wird, die in der Vererbungskette vor derjenigen liegt, die diese Methode definiert. Sie kann in mehreren Klassen in der Kette vor ihr nicht-virtuell definiert werden, bevor die Virtualität für eine Methode dieses Namens und dieser Signatur beginnt, und sie werden ihre eigenen Methoden verwenden, wenn sie referenziert werden (und alle Klassen nach dieser Definition in der Kette werden diese Definition verwenden, wenn sie keine eigene Definition haben, im Gegensatz zu virtual, das immer die endgültige Definition verwendet). Wenn eine Methode als virtuell deklariert wird, muss sie in dieser Klasse oder einer abgeleiteten Klasse in der Vererbungskette für das vollständige Objekt, das konstruiert wurde, implementiert werden, um verwendet werden zu können.

override ist ein weiterer Compilerschutz, der besagt, dass diese Funktion etwas überschreibt, und wenn das nicht der Fall ist, einen Compilerfehler auslöst.

= 0 bedeutet, dass es sich um eine abstrakte Funktion handelt

final verhindert, dass eine virtuelle Funktion in einer weiter abgeleiteten Klasse erneut implementiert wird, und stellt sicher, dass die virtuelle Tabelle der am weitesten abgeleiteten Klasse die endgültige Funktion dieser Klasse enthält.

= default macht in der Dokumentation deutlich, dass der Compiler die Standardimplementierung verwendet

= delete einen Compiler-Fehler auslösen, wenn ein Aufruf dieser Funktion versucht wird

Wenn Sie eine nicht-virtuelle Funktion aufrufen, wird sie zur richtigen Methodendefinition aufgelöst, ohne die virtuelle Tabelle zu durchlaufen. Wenn Sie eine virtuelle Funktion aufrufen, deren endgültige Definition in einer geerbten Klasse liegt, wird deren virtuelle Tabelle verwendet und das Unterobjekt automatisch an sie übergeben, wenn Sie den Objektzeiger beim Aufruf der Methode nicht auf diesen Typ casten. Wenn Sie eine virtuelle Funktion, die in der am weitesten abgeleiteten Klasse definiert ist, mit einem Zeiger dieses Typs aufrufen, verwendet sie ihre virtuelle Tabelle, die sich am Anfang des Objekts befindet. Wenn Sie sie auf einem Zeiger eines geerbten Typs aufrufen und die Funktion in dieser Klasse ebenfalls virtuell ist, dann wird sie den vtable-Zeiger dieses Unterobjekts verwenden, der im Fall des ersten Unterobjekts derselbe Zeiger sein wird wie der der am weitesten abgeleiteten Klasse, der keinen Thunk enthalten wird, da die Adresse des Objekts und des Unterobjekts dieselbe ist, und daher ist es genauso einfach, wie wenn die Methode diesen Zeiger automatisch umbesetzt, aber im Fall eines 2, wird seine vtable einen nicht-virtuellen Thunk enthalten, um den Zeiger des Objekts vom geerbten Typ in den Typ zu konvertieren, den die Implementierung in der am meisten abgeleiteten Klasse erwartet, nämlich das vollständige Objekt, und daher den Zeiger des Unterobjekts so verschieben, dass er auf das vollständige Objekt zeigt, und im Fall des Basis-Unterobjekts einen virtuellen Thunk erfordern, um den Zeiger auf die Basis auf das vollständige Objekt zu verschieben, so dass er vom Parametertyp des versteckten Objekts der Methode umbesetzt werden kann.

Die Verwendung des Objekts mit einem Referenzoperator und nicht durch einen Zeiger (Dereferenzoperator) bricht den Polymorphismus und behandelt virtuelle Methoden wie reguläre Methoden. Dies liegt daran, dass polymorphes Casting bei Nicht-Zeigertypen aufgrund von Slicing nicht möglich ist.

Virtuelle Vererbung

Erwägen Sie

class Base
  {
      int a = 1;
      int b = 2;
  public:
      void virtual CommonFunction(){} ; //define empty method body
      void virtual VirtualFunction(){} ;
  };

class DerivedClass1: virtual public Base
  {
      int c = 3;
  public:
    void virtual DerivedCommonFunction(){} ;
     void virtual VirtualFunction(){} ;
  };

  class DerivedClass2 : virtual public Base
 {
     int d = 4;
 public:
     //void virtual DerivedCommonFunction(){} ;    
     void virtual VirtualFunction(){} ;
     void virtual DerivedCommonFunction2(){} ;
 };

class DerivedDerivedClass :  public DerivedClass1, public DerivedClass2
 {
   int e = 5;
 public:
     void virtual DerivedDerivedCommonFunction(){} ;
     void virtual VirtualFunction(){} ;
 };

 int main () {
   DerivedDerivedClass* d = new DerivedDerivedClass;
   d->VirtualFunction();
   d->DerivedCommonFunction();
   d->DerivedCommonFunction2();
   d->DerivedDerivedCommonFunction();
   ((DerivedClass2*)d)->DerivedCommonFunction2();
   ((Base*)d)->VirtualFunction();
 }

Wenn Sie die Klasse bass nicht virtuell erben, erhalten Sie ein Objekt, das wie folgt aussieht:

Stattdessen:

D.h. es wird 2 Basisobjekte geben.

In der obigen Situation der Vererbung eines virtuellen Diamanten, nachdem new aufgerufen wird, übergibt er die Adresse des zugewiesenen Speicherplatzes für das Objekt an den am weitesten abgeleiteten Konstruktor DerivedDerivedClass::DerivedDerivedClass() , die die Base::Base() zuerst, die ihre vtable in das dedizierte Unterobjekt der Basis schreibt, dann DerivedDerivedClass::DerivedDerivedClass() ruft auf. DerivedClass1::DerivedClass1() das seinen Zeiger auf die virtuelle Tabelle in sein Unterobjekt schreibt und den Zeiger des Basisunterobjekts am Ende des Objekts durch Konsultation der übergebenen VTT überschreibt, und ruft dann DerivedClass1::DerivedClass1() dasselbe zu tun, und schließlich DerivedDerivedClass::DerivedDerivedClass() überschreibt alle 3 Zeiger mit seinem virtuellen Tabellenzeiger für diese geerbte Klasse. Dies ist anstelle von (wie im 1. Bild oben dargestellt) DerivedDerivedClass::DerivedDerivedClass() aufrufen DerivedClass1::DerivedClass1() und dass der Aufruf Base::Base() (wodurch der virtuelle Zeiger überschrieben wird), Rückkehr, Verschieben der Adresse zum nächsten Unterobjekt, Aufruf von DerivedClass2::DerivedClass2() und dann auch der Aufruf Base::Base() , überschreibt diesen virtuellen Zeiger, kehrt zurück und DerivedDerivedClass Konstruktor, der beide virtuellen Zeiger mit seinem virtuellen Tabellenzeiger überschreibt (in diesem Fall enthält die virtuelle Tabelle des am weitesten abgeleiteten Konstruktors 2 Untertabellen anstelle von 3).

Das Folgende wird im Debug-Modus -O0 kompiliert, so dass es redundante Assemblierung gibt

main:
.LFB8:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        mov     edi, 48 //pass size to new
        call    operator new(unsigned long) //call new
        mov     rbx, rax  //move the address of the allocation to rbx
        mov     rdi, rbx  //move it to rdi i.e. pass to the call
        call    DerivedDerivedClass::DerivedDerivedClass() [complete object constructor] //construct on this address
        mov     QWORD PTR [rbp-24], rbx  //store the address of the object on the stack as the d pointer variable on -O0, will be optimised off on -Ofast if the address of the pointer itself isn't taken in the code, because this address does not need to be on the stack, it can just be passed in a register to the subsequent methods

Nebenbei bemerkt: Wäre der Code DerivedDerivedClass d = DerivedDerivedClass() die main Funktion würde wie folgt aussehen:

main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 48 // make room for and zero 48 bytes on the stack for the 48 byte object, no extra padding required as the frame is 64 bytes with `rbp` and return address of the function it calls (no stack params are passed to any function it calls), hence rsp will be aligned by 16 assuming it was aligned at the start of this frame
        mov     QWORD PTR [rbp-48], 0
        mov     QWORD PTR [rbp-40], 0
        mov     QWORD PTR [rbp-32], 0
        mov     QWORD PTR [rbp-24], 0
        mov     QWORD PTR [rbp-16], 0
        mov     QWORD PTR [rbp-8], 0
        lea     rax, [rbp-48] // load the address of the cleared 48 bytes
        mov     rdi, rax // pass the address as a pointer to the 48 bytes cleared as the first parameter to the constructor
        call    DerivedDerivedClass::DerivedDerivedClass() [complete object constructor]
        //address is not stored on the stack because the object is used directly -- there is no pointer variable -- d refers to the object on the stack as opposed to being a pointer

Um auf das ursprüngliche Beispiel zurückzukommen, die DerivedDerivedClass Konstrukteur:

DerivedDerivedClass::DerivedDerivedClass() [complete object constructor]:
.LFB20:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
.LBB5:
        mov     rax, QWORD PTR [rbp-8] // object address now in rax 
        add     rax, 32 //increment address by 32
        mov     rdi, rax // move object address+32 to rdi i.e. pass to call
        call    Base::Base() [base object constructor]
        mov     rax, QWORD PTR [rbp-8] //move object address to rax
        mov     edx, OFFSET FLAT:VTT for DerivedDerivedClass+8 //move address of VTT+8 to edx
        mov     rsi, rdx //pass VTT+8 address as 2nd parameter 
        mov     rdi, rax //object address as first (DerivedClass1 subobject)
        call    DerivedClass1::DerivedClass1() [base object constructor]
        mov     rax, QWORD PTR [rbp-8] //move object address to rax
        add     rax, 16  //increment object address by 16
        mov     edx, OFFSET FLAT:VTT for DerivedDerivedClass+24  //store address of VTT+24 in edx
        mov     rsi, rdx //pass address of VTT+24 as second parameter
        mov     rdi, rax //address of DerivedClass2 subobject as first
        call    DerivedClass2::DerivedClass2() [base object constructor]
        mov     edx, OFFSET FLAT:vtable for DerivedDerivedClass+24 //move this to edx
        mov     rax, QWORD PTR [rbp-8] // object address now in rax
        mov     QWORD PTR [rax], rdx. //store address of vtable for DerivedDerivedClass+24 at the start of the object
        mov     rax, QWORD PTR [rbp-8] // object address now in rax
        add     rax, 32  // increment object address by 32
        mov     edx, OFFSET FLAT:vtable for DerivedDerivedClass+120 //move this to edx
        mov     QWORD PTR [rax], rdx  //store vtable for DerivedDerivedClass+120 at object+32 (Base) 
        mov     edx, OFFSET FLAT:vtable for DerivedDerivedClass+72 //store this in edx
        mov     rax, QWORD PTR [rbp-8] //move object address to rax
        mov     QWORD PTR [rax+16], rdx //store vtable for DerivedDerivedClass+72 at object+16 (DerivedClass2)
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+28], 5 // stores e = 5 in the object
.LBE5:
        nop
        leave
        ret

El DerivedDerivedClass Konstruktoraufrufe Base::Base() mit einem Zeiger auf das Objekt offset 32. Base speichert einen Zeiger auf seine virtuelle Tabelle an der Adresse, die es erhält, und seine Mitglieder danach.

Base::Base() [base object constructor]:
.LFB11:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi //stores address of object on stack (-O0)
.LBB2:
        mov     edx, OFFSET FLAT:vtable for Base+16  //puts vtable for Base+16 in edx
        mov     rax, QWORD PTR [rbp-8] //copies address of object from stack to rax
        mov     QWORD PTR [rax], rdx  //stores it address of object
        mov     rax, QWORD PTR [rbp-8] //copies address of object on stack to rax again
        mov     DWORD PTR [rax+8], 1 //stores a = 1 in the object
        mov     rax, QWORD PTR [rbp-8] //junk from -O0
        mov     DWORD PTR [rax+12], 2  //stores b = 2 in the object
.LBE2:
        nop
        pop     rbp
        ret

DerivedDerivedClass::DerivedDerivedClass() ruft dann DerivedClass1::DerivedClass1() mit einem Zeiger auf den Objekt-Offset 0 und übergibt außerdem die Adresse von VTT for DerivedDerivedClass+8

DerivedClass1::DerivedClass1() [base object constructor]:
.LFB14:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi //address of object
        mov     QWORD PTR [rbp-16], rsi  //address of VTT+8
.LBB3:
        mov     rax, QWORD PTR [rbp-16]  //address of VTT+8 now in rax
        mov     rdx, QWORD PTR [rax]     //address of DerivedClass1-in-DerivedDerivedClass+24 now in rdx
        mov     rax, QWORD PTR [rbp-8]   //address of object now in rax
        mov     QWORD PTR [rax], rdx     //store address of DerivedClass1-in-.. in the object
        mov     rax, QWORD PTR [rbp-8]  // address of object now in rax
        mov     rax, QWORD PTR [rax]    //address of DerivedClass1-in.. now implicitly in rax
        sub     rax, 24                 //address of DerivedClass1-in-DerivedDerivedClass+0 now in rax
        mov     rax, QWORD PTR [rax]    //value of 32 now in rax
        mov     rdx, rax                // now in rdx
        mov     rax, QWORD PTR [rbp-8]  //address of object now in rax
        add     rdx, rax                //address of object+32 now in rdx
        mov     rax, QWORD PTR [rbp-16]  //address of VTT+8 now in rax
        mov     rax, QWORD PTR [rax+8]   //derference VTT+8+8; address of DerivedClass1-in-DerivedDerivedClass+72 (Base::CommonFunction()) now in rax
        mov     QWORD PTR [rdx], rax     //store at address object+32 (offset to Base)
        mov     rax, QWORD PTR [rbp-8]  //store address of object in rax, return
        mov     DWORD PTR [rax+8], 3    //store its attribute c = 3 in the object
.LBE3:
        nop
        pop     rbp
        ret

VTT for DerivedDerivedClass:
        .quad   vtable for DerivedDerivedClass+24
        .quad   construction vtable for DerivedClass1-in-DerivedDerivedClass+24 //(DerivedClass1 uses this to write its vtable pointer)
        .quad   construction vtable for DerivedClass1-in-DerivedDerivedClass+72 //(DerivedClass1 uses this to overwrite the base vtable pointer)
        .quad   construction vtable for DerivedClass2-in-DerivedDerivedClass+24
        .quad   construction vtable for DerivedClass2-in-DerivedDerivedClass+72
        .quad   vtable for DerivedDerivedClass+120 // DerivedDerivedClass supposed to use this to overwrite Bases's vtable pointer
        .quad   vtable for DerivedDerivedClass+72 // DerivedDerivedClass supposed to use this to overwrite DerivedClass2's vtable pointer
//although DerivedDerivedClass uses vtable for DerivedDerivedClass+72 and DerivedDerivedClass+120 directly to overwrite them instead of going through the VTT

construction vtable for DerivedClass1-in-DerivedDerivedClass:
        .quad   32
        .quad   0
        .quad   typeinfo for DerivedClass1
        .quad   DerivedClass1::DerivedCommonFunction()
        .quad   DerivedClass1::VirtualFunction()
        .quad   -32
        .quad   0
        .quad   -32
        .quad   typeinfo for DerivedClass1
        .quad   Base::CommonFunction()
        .quad   virtual thunk to DerivedClass1::VirtualFunction()
construction vtable for DerivedClass2-in-DerivedDerivedClass:
        .quad   16
        .quad   0
        .quad   typeinfo for DerivedClass2
        .quad   DerivedClass2::VirtualFunction()
        .quad   DerivedClass2::DerivedCommonFunction2()
        .quad   -16
        .quad   0
        .quad   -16
        .quad   typeinfo for DerivedClass2
        .quad   Base::CommonFunction()
        .quad   virtual thunk to DerivedClass2::VirtualFunction()
vtable for DerivedDerivedClass:
        .quad   32
        .quad   0
        .quad   typeinfo for DerivedDerivedClass
        .quad   DerivedClass1::DerivedCommonFunction()
        .quad   DerivedDerivedClass::VirtualFunction()
        .quad   DerivedDerivedClass::DerivedDerivedCommonFunction()
        .quad   16
        .quad   -16
        .quad   typeinfo for DerivedDerivedClass
        .quad   non-virtual thunk to DerivedDerivedClass::VirtualFunction()
        .quad   DerivedClass2::DerivedCommonFunction2()
        .quad   -32
        .quad   0
        .quad   -32
        .quad   typeinfo for DerivedDerivedClass
        .quad   Base::CommonFunction()
        .quad   virtual thunk to DerivedDerivedClass::VirtualFunction()

virtual thunk to DerivedClass1::VirtualFunction():
        mov     r10, QWORD PTR [rdi]
        add     rdi, QWORD PTR [r10-32]
        jmp     .LTHUNK0
virtual thunk to DerivedClass2::VirtualFunction():
        mov     r10, QWORD PTR [rdi]
        add     rdi, QWORD PTR [r10-32]
        jmp     .LTHUNK1
virtual thunk to DerivedDerivedClass::VirtualFunction():
        mov     r10, QWORD PTR [rdi]
        add     rdi, QWORD PTR [r10-32]
        jmp     .LTHUNK2
non-virtual thunk to DerivedDerivedClass::VirtualFunction():
        sub     rdi, 16
        jmp     .LTHUNK3

        .set    .LTHUNK0,DerivedClass1::VirtualFunction()
        .set    .LTHUNK1,DerivedClass2::VirtualFunction()
        .set    .LTHUNK2,DerivedDerivedClass::VirtualFunction()
        .set    .LTHUNK3,DerivedDerivedClass::VirtualFunction()

Jede geerbte Klasse hat ihre eigene virtuelle Konstruktionstabelle und die am meisten abgeleitete Klasse, DerivedDerivedClass hat eine virtuelle Tabelle mit einer Untertabelle für jedes Teilobjekt und verwendet den Zeiger auf die Untertabelle, um den Zeiger auf die Konstruktion vtable zu überschreiben, den der Konstruktor der geerbten Klasse für jedes Teilobjekt gespeichert hat. Jede virtuelle Methode, die einen Thunk benötigt (ein virtueller Thunk verschiebt den Objektzeiger von der Basis auf den Anfang des Objekts und ein nicht-virtueller Thunk verschiebt den Objektzeiger von einem Objekt der geerbten Klasse, das nicht das Basisobjekt ist, auf den Anfang des gesamten Objekts vom Typ DerivedDerivedClass ). El DerivedDerivedClass Konstruktor verwendet auch eine virtuelle Tabellentabelle (VTT) als serielle Liste aller virtuellen Tabellenzeiger, die er verwenden muss, und übergibt sie an jeden Konstruktor (zusammen mit der Adresse des Unterobjekts, für das der Konstruktor bestimmt ist), die sie verwenden, um ihren und den vtable-Zeiger der Basis zu überschreiben.

DerivedDerivedClass::DerivedDerivedClass() übergibt dann die Adresse des Objekts+16 und die Adresse von VTT für DerivedDerivedClass+24 a DerivedClass2::DerivedClass2() dessen Aufbau identisch ist mit DerivedClass1::DerivedClass1() mit Ausnahme der Zeile mov DWORD PTR [rax+8], 3 die offensichtlich eine 4 statt einer 3 hat für d = 4 .

Danach werden alle 3 virtuellen Tabellenzeiger im Objekt durch Zeiger auf Offsets in DerivedDerivedClass vtable in die Repräsentation für diese Klasse.

Die Aufforderung an d->VirtualFunction() en main :

        mov     rax, QWORD PTR [rbp-24] //store pointer to object (and hence vtable pointer) in rax
        mov     rax, QWORD PTR [rax] //dereference this pointer to vtable pointer and store virtual table pointer in rax
        add     rax, 8 // add 8 to the pointer to get the 2nd function pointer in the table
        mov     rdx, QWORD PTR [rax] //dereference this pointer to get the address of the method to call
        mov     rax, QWORD PTR [rbp-24] //restore pointer to object in rax (-O0 is inefficient, yes)
        mov     rdi, rax  //pass object to the method
        call    rdx

d->DerivedCommonFunction(); :

        mov     rax, QWORD PTR [rbp-24]
        mov     rdx, QWORD PTR [rbp-24]
        mov     rdx, QWORD PTR [rdx]
        mov     rdx, QWORD PTR [rdx]
        mov     rdi, rax  //pass object to method
        call    rdx  //call the first function in the table

d->DerivedCommonFunction2(); :

        mov     rax, QWORD PTR [rbp-24] //get the object pointer
        lea     rdx, [rax+16]  //get the address of the 2nd subobject in the object
        mov     rax, QWORD PTR [rbp-24] //get the object pointer
        mov     rax, QWORD PTR [rax+16] // get the vtable pointer of the 2nd subobject
        add     rax, 8  //call the 2nd function in this table
        mov     rax, QWORD PTR [rax]  //get the address of the 2nd function
        mov     rdi, rdx  //call it and pass the 2nd subobject to it
        call    rax

d->DerivedDerivedCommonFunction(); :

        mov     rax, QWORD PTR [rbp-24] //get the object pointer
        mov     rax, QWORD PTR [rax] //get the vtable pointer
        add     rax, 16 //get the 3rd function in the first virtual table (which is where virtual functions that that first appear in the most derived class go, because they belong to the full object which uses the virtual table pointer at the start of the object)
        mov     rdx, QWORD PTR [rax] //get the address of the object
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax  //call it and pass the whole object to it
        call    rdx

((DerivedClass2*)d)->DerivedCommonFunction2(); :

//it casts the object to its subobject and calls the corresponding method in its virtual table, which will be a non-virtual thunk

        cmp     QWORD PTR [rbp-24], 0
        je      .L14
        mov     rax, QWORD PTR [rbp-24]
        add     rax, 16
        jmp     .L15
.L14:
        mov     eax, 0
.L15:
        cmp     QWORD PTR [rbp-24], 0
        cmp     QWORD PTR [rbp-24], 0
        je      .L18
        mov     rdx, QWORD PTR [rbp-24]
        add     rdx, 16
        jmp     .L19
.L18:
        mov     edx, 0
.L19:
        mov     rdx, QWORD PTR [rdx]
        add     rdx, 8
        mov     rdx, QWORD PTR [rdx]
        mov     rdi, rax
        call    rdx

((Base*)d)->VirtualFunction(); :

//it casts the object to its subobject and calls the corresponding function in its virtual table, which will be a virtual thunk

        cmp     QWORD PTR [rbp-24], 0
        je      .L20
        mov     rax, QWORD PTR [rbp-24]
        mov     rax, QWORD PTR [rax]
        sub     rax, 24
        mov     rax, QWORD PTR [rax]
        mov     rdx, rax
        mov     rax, QWORD PTR [rbp-24]
        add     rax, rdx
        jmp     .L21
.L20:
        mov     eax, 0
.L21:
        cmp     QWORD PTR [rbp-24], 0
        cmp     QWORD PTR [rbp-24], 0
        je      .L24
        mov     rdx, QWORD PTR [rbp-24]
        mov     rdx, QWORD PTR [rdx]
        sub     rdx, 24
        mov     rdx, QWORD PTR [rdx]
        mov     rcx, rdx
        mov     rdx, QWORD PTR [rbp-24]
        add     rdx, rcx
        jmp     .L25
.L24:
        mov     edx, 0
.L25:
        mov     rdx, QWORD PTR [rdx]
        add     rdx, 8
        mov     rdx, QWORD PTR [rdx]
        mov     rdi, rax
        call    rdx

1voto

wilhelmtell Punkte 55189

Dies bedeutet, dass ein Aufruf einer virtuellen Funktion an die "richtige" Klasse weitergeleitet wird.

C++ FAQ Lite FTW.

Kurz gesagt, sie wird häufig in Szenarien der Mehrfachvererbung verwendet, in denen eine "diamantene" Hierarchie gebildet wird. Die virtuelle Vererbung löst dann die in der untersten Klasse entstandene Mehrdeutigkeit auf, wenn Sie eine Funktion in dieser Klasse aufrufen und die Funktion entweder in der Klasse D1 oder D2 über dieser untersten Klasse aufgelöst werden muss. Siehe die FAQ-Punkt für ein Diagramm und Details.

Es wird auch verwendet in Schwesterdelegation eine leistungsstarke Funktion (allerdings nichts für schwache Nerven). Siehe este FAQ.

Siehe auch Punkt 40 in Effective C++ 3rd edition (43 in 2nd edition).

1voto

Baltimark Punkte 8606

Sie sind ein wenig verwirrend. Ich weiß nicht, ob du einige Begriffe verwechselst.

Sie haben keine virtuelle Basisklasse in Ihrem OP. Sie haben nur eine Basisklasse.

Sie haben virtuell geerbt. Dies wird in der Regel bei der Mehrfachvererbung verwendet, damit mehrere abgeleitete Klassen die Elemente der Basisklasse verwenden, ohne sie zu reproduzieren.

Eine Basisklasse mit einer reinen virtuellen Funktion wird nicht instanziiert. dies erfordert die Syntax, auf die Paul kommt. Sie wird normalerweise verwendet, damit abgeleitete Klassen diese Funktionen definieren müssen.

Ich möchte das nicht weiter erläutern, weil ich nicht ganz verstehe, worum es Ihnen geht.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X