Wat is in C++ een virtuele basisklasse?

Ik wil weten wat een “virtuele basisklasse” is en wat het betekent.

Laat me een voorbeeld laten zien:

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

Antwoord 1, autoriteit 100%

Virtuele basisklassen, gebruikt bij virtuele overerving, is een manier om te voorkomen dat meerdere “instanties” van een bepaalde klasse in een overervingshiërarchie verschijnen bij gebruik van meervoudige overerving.

Overweeg het volgende scenario:

class A { public: void Foo() {} };
class B : public A {};
class C : public A {};
class D : public B, public C {};

De bovenstaande klassenhiërarchie resulteert in de “gevreesde diamant” die er als volgt uitziet:

 A
 / \
B   C
 \ /
  D

Een instantie van D zal bestaan uit B, die A omvat, en C die ook A omvat. U hebt dus twee “instanties” (bij gebrek aan een betere uitdrukking) van A.

Als je dit scenario hebt, heb je kans op dubbelzinnigheid. Wat gebeurt er als je dit doet:

D d;
d.Foo(); // is this B's Foo() or C's Foo() ??

Virtuele overerving is er om dit probleem op te lossen. Wanneer je virtueel opgeeft bij het erven van je klassen, vertel je de compiler dat je maar één instantie wilt.

class A { public: void Foo() {} };
class B : public virtual A {};
class C : public virtual A {};
class D : public B, public C {};

Dit betekent dat er slechts één “instantie” van A in de hiërarchie is opgenomen. Vandaar

D d;
d.Foo(); // no longer ambiguous

Dit is een mini-samenvatting. Lees voor meer informatie diten dit. Een goed voorbeeld is ook hierbeschikbaar.


Antwoord 2, autoriteit 48%

Over de geheugenlay-out

Als een kanttekening, het probleem met de gevreesde diamant is dat de basisklasse meerdere keren aanwezig is. Dus met een reguliere erfenis, denkt u dat u:

 A
 / \
B   C
 \ /
  D

Maar in de geheugenlay-out heb je:

A   A
|   |
B   C
 \ /
  D

Dit verklaart waarom wanneer u D::foo()aanroept, u een ambiguïteitsprobleem heeft. Maar het echteprobleem doet zich voor wanneer je een lidvariabele van Awilt gebruiken. Laten we bijvoorbeeld zeggen dat we hebben:

class A
{
    public :
       foo() ;
       int m_iValue ;
} ;

Als u probeert toegang te krijgen tot m_iValuevanuit D, zal de compiler protesteren, omdat hij in de hiërarchie twee m_iValue, niet één. En als u er één wijzigt, bijvoorbeeld B::m_iValue(dat is de A::m_iValueouder van B), C::m_iValuewordt niet gewijzigd (dat is de A::m_iValueouder van C).

Dit is waar virtuele overerving van pas komt, want hiermee krijg je terug naar een echte diamantlay-out, met niet alleen één foo()-methode, maar ook één en slechts één m_iValue.

Wat kan er mis gaan?

Stel je voor:

  • Aheeft een basisfunctie.
  • Bvoegt er een soort coole reeks gegevens aan toe (bijvoorbeeld)
  • Cvoegt er een coole functie aan toe, zoals een waarnemerpatroon (bijvoorbeeld op m_iValue).
  • Derft van Ben C, en dus van A.

Bij normale overerving is het wijzigen van m_iValuevan Ddubbelzinnig en dit moet worden opgelost. Zelfs als dat zo is, zijn er twee m_iValuesin D, dus je kunt dat beter onthouden en de twee tegelijkertijd bijwerken.

Met virtuele overerving is het aanpassen van m_iValuevan Doké… Maar… Laten we zeggen dat je Dhebt. Via de C-interface heb je een waarnemer toegevoegd. En via de B-interface werk je de coole array bij, wat als neveneffect heeft dat m_iValue

direct verandert.

Omdat de wijziging van m_iValuedirect wordt gedaan (zonder gebruik te maken van een virtuele toegangsmethode), wordt de waarnemer die “luistert” via Cniet aangeroepen, omdat de code het implementeren van het luisteren is in C, en Bweet er niets van…

Conclusie

Als je een diamant in je hiërarchie hebt, betekent dit dat je 95% kans hebt om iets verkeerds te hebben gedaan met die hiërarchie.


Antwoord 3, autoriteit 7%

Het uitleggen van meervoudige overerving met virtuele bases vereist kennis van het C++ objectmodel. En het onderwerp duidelijk uitleggen kan het beste in een artikel en niet in een commentaarveld.

De beste, leesbare uitleg die ik vond die al mijn twijfels over dit onderwerp oploste, was dit artikel: http://www.phpcompiler.org/articles/virtualinheritance.html

Je hoeft echt niets meer over het onderwerp te lezen (tenzij je een compiler-schrijver bent) nadat je dat hebt gelezen…


Antwoord 4, autoriteit 2%

Een virtuele basisklasse is een klasse die
kan niet worden geïnstantieerd : je kunt niet
maak er direct object van.

Ik denk dat je twee heel verschillende dingen door elkaar haalt. Virtuele overerving is niet hetzelfde als een abstracte klasse. Virtuele overerving wijzigt het gedrag van functieaanroepen; soms lost het functieaanroepen op die anders dubbelzinnig zouden zijn, soms stelt het de behandeling van functieaanroepen uit naar een andere klasse dan men zou verwachten bij een niet-virtuele overerving.


Antwoord 5

Ik wil graag iets toevoegen aan de vriendelijke verduidelijkingen van OJ.

Virtuele overerving komt niet zonder een prijs. Zoals met alle virtuele dingen, krijg je een prestatiehit. Er is een manier om deze prestatiehit te omzeilen die mogelijk minder elegant is.

In plaats van de diamant te breken door virtueel af te leiden, kun je een andere laag aan de diamant toevoegen, om zoiets als dit te krijgen:

  B
  / \
D11 D12
 |   |
D21 D22
 \   /
  DD

Geen van de klassen erfende virtueel, alles erfgenaam in het openbaar. Klassen D21 en D22 verbergen dan virtuele functie F () die dubbelzinnig is voor DD, misschien door de functie privé te verklaren. Ze definiëren elk een wikkelfunctie, F1 () en F2 () respectievelijk elke calling-lokale (privé) f (), waardoor conflicten worden opgelost. Klasse DD-oproepen F1 () als het wil D11 :: F () en F2 () als het wil D12 :: F (). Als u de Wrappers Inline definieert, krijgt u waarschijnlijk over nul overhead.

Natuurlijk, als u D11 en D12 kunt wijzigen, dan kunt u dezelfde truc in deze klassen doen, maar vaak is dat niet het geval.


Antwoord 6

Naast wat al is gezegd over meerdere en virtuele erfenis (en), is er een zeer interessant artikel over DR DOBB’s Journal: Meerdere erfenis beschouwd als nuttig


Antwoord 7

Diamant Orditance Runnable Gebruik voorbeeld

Dit voorbeeld laat zien hoe u een virtuele basisklasse in het typische scenario gebruikt: om diamant-overervingsproblemen op te lossen.

Overweeg het volgende werkvoorbeeld:

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

Compileren en uitvoeren:

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

Als we de virtualverwijderen in:

class B : public virtual A

we zouden een muur van fouten krijgen over het feit dat GCC D-leden en methoden die tweemaal via A zijn overgenomen, niet kan oplossen:

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

Getest op GCC 9.3.0, Ubuntu 20.04.


Antwoord 8

Regelmatige overerving

Met typische 3-niveau niet-diamant niet-virtuele-overerving, wanneer u een nieuw meest afgeleide-object instantieert, wordt newaangeroepen en wordt de vereiste grootte voor het object op de heap opgelost van het klassetype door de compiler en doorgegeven aan nieuw.

newheeft een handtekening:

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

En belt naar mallocen geeft de lege aanwijzer terug

Dit adres wordt vervolgens doorgegeven aan de constructor van het meest afgeleide object, die onmiddellijk de middelste constructor zal aanroepen en vervolgens zal de middelste constructor onmiddellijk de basisconstructor aanroepen. De basis slaat dan een aanwijzer op naar zijn virtuele tabel aan het begin van het object en vervolgens zijn attributen erna. Dit keert dan terug naar de middelste constructor die zijn virtuele tabelaanwijzer op dezelfde locatie zal opslaan en vervolgens zijn attributen na de attributen die door de basisconstructor zouden zijn opgeslagen. Vervolgens keert het terug naar de meest afgeleide constructor, die een aanwijzer naar zijn virtuele tabel op dezelfde locatie opslaat en vervolgens zijn attributen opslaat na de attributen die door de middelste constructor zouden zijn opgeslagen.

Omdat de virtuele tabelaanwijzer wordt overschreven, is de virtuele tabelaanwijzer altijd die van de meest afgeleide klasse. Virtualiteit plant zich voort naar de meest afgeleide klasse, dus als een functie virtueel is in de middenklasse, zal deze virtueel zijn in de meest afgeleide klasse, maar niet in de basisklasse. Als u polymorf een instantie van de meest afgeleide klasse cast naar een pointer naar de basisklasse, zal de compiler dit niet omzetten in een indirecte aanroep van de virtuele tabel en in plaats daarvan de functie rechtstreeks aanroepen A::function(). Als een functie virtueel is voor het type waarnaar je hem hebt gecast, wordt hij omgezet in een aanroep naar de virtuele tabel, die altijd die van de meest afgeleide klasse zal zijn. Als het niet virtueel is voor dat type, roept het gewoon Type::function()aan en geeft het de objectaanwijzer ernaartoe, cast naar Type.

Als ik de aanwijzer naar de virtuele tafel zeg, is dat eigenlijk altijd een offset van 16 in de virtuele tafel.

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

virtualis niet opnieuw vereist in meer afgeleide klassen als het virtueel is in een minder afgeleide klasse omdat het naar beneden verspreidt in de richting van de meest afgeleide klasse. Maar het kan worden gebruikt om aan te tonen dat de functie inderdaad een virtuele functie is, zonder de klassen te hoeven controleren, het typed definities van het type. Wanneer een functie virtueel wordt verklaard, wordt vanaf dat moment alleen de laatste implementatie in de overervingketen gebruikt, maar daarvoor kan het nog steeds niet-virtueel worden gebruikt als het object daarvoor in de erfenisketen die die methode definieert. Het kan niet-virtueel in meerdere klassen worden gedefinieerd in de keten voordat het virtualiteit begint voor een methode van die naam en handtekening, en ze zullen hun eigen methoden gebruiken wanneer ze worden verwezen (en alle klassen na die definitie in de keten die na die definitie in de keten in de keten zullen gebruiken, definitie als ze hun eigen definitie niet hebben, in tegenstelling tot virtueel, dat altijd de definitieve definitie gebruikt). Wanneer een methode virtueel wordt gedeclareerd, moet deze in die klasse of een meer afgeleide klasse in de erfenisketen worden geïmplementeerd voor het volledige object dat is geconstrueerd om te worden gebruikt.

overrideis een andere compiler-bewaker die zegt dat deze functie iets overschrijft en als het niet een compiler-fout gooit.

= 0betekent dat dit een abstracte functie is

finalVoorkomt dat een virtuele functie opnieuw wordt geïmplementeerd in een meer afgeleide klasse en zorgt ervoor dat de virtuele tabel van de meest afgeleide klasse de uiteindelijke functie van die klasse bevat.

= defaultmaakt expliciet in documentatie dat de compiler de standaardimplementatie zal gebruiken

= deletegeeft een compilerfout als een poging hiertoe wordt gedaan

Als u een niet-virtuele functie aanroept, wordt deze omgezet naar de juiste methodedefinitie zonder door de virtuele tabel te gaan. Als je een virtuele functie aanroept die zijn uiteindelijke definitie heeft in een geërfde klasse, dan zal deze zijn virtuele tabel gebruiken en het subobject er automatisch aan doorgeven als je de objectaanwijzer niet naar dat type cast bij het aanroepen van de methode. Als u een virtuele functie aanroept die is gedefinieerd in de meest afgeleide klasse op een aanwijzer van dat type, zal deze zijn virtuele tabel gebruiken, die aan het begin van het object zal zijn. Als je het aanroept op een aanwijzer van een geërfd type en de functie is ook virtueel in die klasse, dan zal het de vtable-aanwijzer van dat subobject gebruiken, wat in het geval van het eerste subobject dezelfde aanwijzer zal zijn als de meest afgeleide klasse, die geen thunk zal bevatten omdat het adres van het object en het subobject hetzelfde zijn, en daarom is het net zo eenvoudig als de methode die deze aanwijzer automatisch herschikt, maar in het geval van een 2e subobject bevat de vtable een niet- virtuele thunk om de aanwijzer van het object van het geërfde type te converteren naar het type dat de implementatie in de meest afgeleide klasse verwacht, wat het volledige object is, en verschuift daarom de aanwijzer van het subobject om naar het volledige object te wijzen, en in het geval van een basissubobject , zal een virtuele thunk nodig hebben om de aanwijzer naar de basis te verplaatsen naar het volledige object, zodat het kan worden herschikt door de methode verborgen object parametertype.

Het object gebruiken met een referentie-operator en niet via een aanwijzer (dereference-operator) verbreekt polymorfisme en behandelt virtuele methoden als reguliere methoden. Dit komt omdat polymorf gieten op niet-aanwijzertypes niet kan plaatsvinden vanwege slicing.

Virtuele overerving

Overweeg

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

Zonder de basklasse virtueel over te nemen, krijgt u een object dat er als volgt uitziet:

In plaats van dit:

D.w.z. er zullen 2 basisobjecten zijn.

In de virtuele diamantovende situatie hierboven, wordt het na newgenoemd, het adres van de toegewezen ruimte voor het object naar de meest afgeleide constructeur DerivedDerivedClass::DerivedDerivedClass(), die Base::Base()eerste, die zijn vtable in de speciale subobject van de basis schrijft, is het dan DerivedDerivedClass::DerivedDerivedClass()Oproepen DerivedClass1::DerivedClass1(), die zijn virtuele tabelaanwijzer op zijn subobject schrijft en de aanwijzer van het basissignobewerking aan het einde van het object aan het einde van het object overschrijdt door de doorgegeven VTT te raadplegen en vervolgens opdringt DerivedClass1::DerivedClass1()om hetzelfde te doen, en tot slot DerivedDerivedClass::DerivedDerivedClass()Overschrijft alle 3 Pointers met zijn virtuele tabelaanwijzer voor die erfelijke klasse. Dit is in plaats van (zoals geïllustreerd in het 1e afbeelding hierboven) DerivedDerivedClass::DerivedDerivedClass()Bellen DerivedClass1::DerivedClass1()en dat belt u Base::Base()(die de virtuele aanwijzer overschrijft), retourneren, waarbij het adres naar de volgende subobject wordt gecompenseerd, het toevoegen van DerivedClass2::DerivedClass2()en vervolgens bellen met Base::Base(), overschrijven die virtuele aanwijzer, terugkerend en vervolgens DerivedDerivedClassConstructor overschrijft beide virtuele aanwijzingen met zijn virtuele tabelaanwijzer (in dit geval bevat de virtuele tabel van de meest afgeleide constructeur in plaats daarvan 2 subtabellen van 3).

Het volgende is allemaal samengesteld in debugmodus -O0, dus er zal een redundante montage

zijn

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

Als de code tussen haakjes DerivedDerivedClass d = DerivedDerivedClass()zou zijn, zou de functie mainer als volgt uitzien:

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

Teruggaand naar het oorspronkelijke voorbeeld, de DerivedDerivedClassconstructor:

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

De DerivedDerivedClassconstructor roept Base::Base()aan met een pointer naar de object offset 32. Base slaat een pointer op naar zijn virtuele tabel op het adres dat het ontvangt en zijn leden erachter.

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()roept vervolgens DerivedClass1::DerivedClass1()aan met een pointer naar de objectoffset 0 en geeft ook het adres van 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()

Elke geërfde klasse heeft zijn eigen virtuele constructietabel en de meest afgeleide klasse, DerivedDerivedClass, heeft een virtuele tabel met een subtabel voor elke, en gebruikt de aanwijzer naar de subtabel om de bouwvtable-aanwijzer te overschrijven dat de constructor van de geërfde klasse voor elk subobject is opgeslagen. Elke virtuele methode die een thunk nodig heeft (virtuele thunk verschuift de objectaanwijzer van de basis naar het begin van het object en een niet-virtuele thunk verschuift de objectaanwijzer van een geërfd klasseobject dat niet het basisobject is naar het begin van de geheel object van het type DerivedDerivedClass). De constructor DerivedDerivedClassgebruikt ook een virtuele tabeltabel (VTT) als een seriële lijst van alle virtuele tabelaanwijzers die hij moet gebruiken en geeft deze door aan elke constructor (samen met het subobjectadres dat de constructor is). for), die ze gebruiken om hun en de vtable-aanwijzer van de basis te overschrijven.

DerivedDerivedClass::DerivedDerivedClass()geeft vervolgens het adres van het object+16 en het adres van VTT voor DerivedDerivedClass+24door aan DerivedClass2::DerivedClass2()waarvan de assembly identiek is aan DerivedClass1::DerivedClass1()behalve de regel mov DWORD PTR [rax+8], 3die duidelijk een 4 heeft van 3 voor d = 4.

Hierna worden alle 3 virtuele tabelaanwijzers in het object vervangen door verwijzingen naar offsets in de vtable van DerivedDerivedClassnaar de representatie voor die klasse.

De aanroep van d->VirtualFunction()in 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

Antwoord 9

Je bent een beetje verwarrend. Ik weet niet of je wat concepten door elkaar haalt.

Je hebt geen virtuele basisklasse in je OP. Je hebt gewoon een basisklasse.

Je hebt virtuele overerving gedaan. Dit wordt meestal gebruikt bij meervoudige overerving, zodat meerdere afgeleide klassen de leden van de basisklasse gebruiken zonder ze te reproduceren.

Een basisklasse met een pure virtuele functie wordt niet geïnstantieerd. dit vereist de syntaxis waar Paul op komt. Het wordt meestal gebruikt zodat afgeleide klassen die functies moeten definiëren.

Ik wil hier niet meer over uitleggen omdat ik niet helemaal begrijp wat je vraagt.


Antwoord 10

Het betekent dat een aanroep van een virtuele functie wordt doorgestuurd naar de “juiste” klasse.

C++ FAQ LiteFTW.

Kortom, het wordt vaak gebruikt in scenario’s met meerdere overervingen, waar een “diamant”-hiërarchie wordt gevormd. Virtuele overerving zal dan de dubbelzinnigheid doorbreken die in de onderste klasse is gecreëerd, wanneer u een functie in die klasse aanroept en de functie moet worden omgezet in klasse D1 of D2 boven die onderste klasse. Zie het FAQ-itemvoor een diagram en details.

Het wordt ook gebruikt in zusterdelegatie, een krachtige functie (hoewel niet voor bangeriken). Zie dezeveelgestelde vragen.

Zie ook item 40 in Effectieve C++ 3e editie (43 in 2e editie).


Antwoord 11

Virtuele klassen zijn niethetzelfde als virtuele overerving. Virtuele klassen die je niet kunt instantiëren, virtuele overerving is iets heel anders.

Wikipedia beschrijft het beter dan ik kan. http://en.wikipedia.org/wiki/Virtual_inheritance

Other episodes