In C++, what is a virtual base class?

后端 未结 11 2164
一个人的身影
一个人的身影 2020-11-22 00:55

I want to know what a \"virtual base class\" is and what it means.

Let me show an example:

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


        
相关标签:
11条回答
  • 2020-11-22 01:10

    Explaining multiple-inheritance with virtual bases requires a knowledge of the C++ object model. And explaining the topic clearly is best done in an article and not in a comment box.

    The best, readable explanation I found that solved all my doubts on this subject was this article: http://www.phpcompiler.org/articles/virtualinheritance.html

    You really won't need to read anything else on the topic (unless you are a compiler writer) after reading that...

    0 讨论(0)
  • 2020-11-22 01:11

    Diamond inheritance runnable usage example

    This example shows how to use a virtual base class in the typical scenario: to solve diamond inheritance problems.

    Consider the following working example:

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

    Compile and run:

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

    If we remove the virtual into:

    class B : public virtual A
    

    we would get a wall of errors about GCC being unable to resolve D members and methods that were inherited twice via A:

    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
    

    Tested on GCC 9.3.0, Ubuntu 20.04.

    0 讨论(0)
  • 2020-11-22 01:15

    A virtual base class is a class that cannot be instantiated : you cannot create direct object out of it.

    I think you are confusing two very different things. Virtual inheritance is not the same thing as an abstract class. Virtual inheritance modifies the behaviour of function calls; sometimes it resolves function calls that otherwise would be ambiguous, sometimes it defers function call handling to a class other than that one would expect in a non-virtual inheritance.

    0 讨论(0)
  • 2020-11-22 01:16

    Regular Inheritance

    With typical 3 level non-diamond non-virtual-inheritance inheritance, when you instantiate a new most-derived-object, new is called and the size required for the object is resolved from the class type by the compiler and passed to new.

    new has a signature:

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

    And makes a call to malloc, returning the void pointer

    This is then passed to the constructor of the most derived object, which will immediately call the middle constructor and then the middle constructor will immediately call the base constructor. The base then stores a pointer to its virtual table at the start of the object and then its attributes after it. This then returns to the middle constructor which will store its virtual table pointer at the same location and then its attributes after the attributes that would have been stored by the base constructor. It the returns to the most derived constructor, which stores a pointer to its virtual table at the same location and and then its attributes after the attributes that would have been stored by the middle constructor.

    Because the virtual table pointer is overwritten, the virtual table pointer ends up always being the one of the most derived class. Virtualness propagates towards the most derived class so if a function is virtual in the middle class, it will be virtual in the most derived class but not the base class. If you polymorphically cast an instance of the most derived class to a pointer to the base class then the compiler will not resolve this to an indirect call to the virtual table and instead will call the function directly A::function(). If a function is virtual for the type you have cast it to then it will resolve to a call into the virtual table which will always be that of the most derived class. If it is not virtual for that type then it will just call Type::function() and pass the object pointer to it, cast to Type.

    Actually when I say pointer to its virtual table, it's actually always an offset of 16 into the virtual table.

    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 is not required again in more-derived classes if it is virtual in a less-derived class because it propagates. But it can be used to show that the function is indeed a virtual function, without having to check the classes it inherits's type definitions.

    override is another compiler guard that says that this function is overriding something and if it isn't then throw a compiler error.

    = 0 means that this is an abstract function

    final prevents a virtual function from being implemented again in a more derived class and will make sure that the virtual table of the most derived class contains the final function of that class.

    = default makes it explicit in documentation that the compiler will use the default implementation

    = delete give a compiler error if a call to this is attempted

    Virtual Inheritance

    Consider

    class Base
      {
          int a = 1;
          int b = 2;
      public:
          void virtual CommonFunction(){} ;
          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();
     }
    

    Without virtually inheriting the bass class you will get an object that looks like this:

    Instead of this:

    I.e. there will be 2 base objects.

    In the virtual diamond inheritance situation above, after new is called, it calls the most derived constructor and in that constructor, it calls all 3 derived constructors passing offsets into its virtual table table, instead of calling just calling DerivedClass1::DerivedClass1() and DerivedClass2::DerivedClass2() and then those both calling Base::Base()

    The following is all compiled in debug mode -O0 so there will be redundant assembly

    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 d
    
    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
            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 object 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
    .LBE5:
            nop
            leave
            ret
    

    It calls Base::Base() with a pointer to the object offset 32. Base stores a pointer to its virtual table at the address it receives and its members after it.

    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() then calls DerivedClass1::DerivedClass1() with a pointer to the object offset 0 and also passes the address of 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]   //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
            .quad   construction vtable for DerivedClass1-in-DerivedDerivedClass+72
            .quad   construction vtable for DerivedClass2-in-DerivedDerivedClass+24
            .quad   construction vtable for DerivedClass2-in-DerivedDerivedClass+72
            .quad   vtable for DerivedDerivedClass+120
            .quad   vtable for DerivedDerivedClass+72
    
    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()
    
    
    

    DerivedDerivedClass::DerivedDerivedClass() then passes the address of the object+16 and the address of VTT for DerivedDerivedClass+24 to DerivedClass2::DerivedClass2() whose assembly is identical to DerivedClass1::DerivedClass1() except for the line mov DWORD PTR [rax+8], 3 which obviously has a 4 instead of 3 for d = 4.

    After this, it replaces all 3 virtual table pointers in the object with pointers to offsets in DerivedDerivedClass's vtable to the representation for that class.

    d->VirtualFunction();:

            mov     rax, QWORD PTR [rbp-24] //store pointer to virtual table in rax 
            mov     rax, QWORD PTR [rax] //dereference and store in rax
            add     rax, 8 // call the 2nd function in the table
            mov     rdx, QWORD PTR [rax] //dereference 
            mov     rax, QWORD PTR [rbp-24]
            mov     rdi, rax
            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
            call    rdx
    

    d->DerivedCommonFunction2();:

            mov     rax, QWORD PTR [rbp-24]
            lea     rdx, [rax+16]
            mov     rax, QWORD PTR [rbp-24]
            mov     rax, QWORD PTR [rax+16]
            add     rax, 8
            mov     rax, QWORD PTR [rax]
            mov     rdi, rdx
            call    rax
    

    d->DerivedDerivedCommonFunction();:

            mov     rax, QWORD PTR [rbp-24]
            mov     rax, QWORD PTR [rax]
            add     rax, 16
            mov     rdx, QWORD PTR [rax]
            mov     rax, QWORD PTR [rbp-24]
            mov     rdi, rax
            call    rdx
    

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

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

            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
    
    0 讨论(0)
  • 2020-11-22 01:17

    You're being a little confusing. I dont' know if you're mixing up some concepts.

    You don't have a virtual base class in your OP. You just have a base class.

    You did virtual inheritance. This is usually used in multiple inheritance so that multiple derived classes use the members of the base class without reproducing them.

    A base class with a pure virtual function is not be instantiated. this requires the syntax that Paul gets at. It is typically used so that derived classes must define those functions.

    I don't want to explain any more about this because I don't totally get what you're asking.

    0 讨论(0)
  • 2020-11-22 01:19

    In addition to what has already been said about multiple and virtual inheritance(s), there is a very interesting article on Dr Dobb's Journal: Multiple Inheritance Considered Useful

    0 讨论(0)
提交回复
热议问题