在C ++中,什么是虚拟基类?

时间:2008-08-22 01:13:15

标签: c++ virtual-inheritance

我想知道什么是“虚拟基类”及其含义。

让我举个例子:

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

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

11 个答案:

答案 0 :(得分:514)

虚拟继承中使用的虚拟基类是一种在使用多重继承时防止出现在继承层次结构中的给定类的多个“实例”的方法。

考虑以下情况:

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

上面的类层次结构导致“可怕的钻石”,如下所示:

  A
 / \
B   C
 \ /
  D

D的实例将由B组成,其中包括A,C也包含A.所以你有两个“实例”(为了更好的表达)A。

当您有这种情况时,您可能会有歧义。当你这样做时会发生什么:

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

虚拟继承可以解决这个问题。当您在继承类时指定virtual时,您告诉编译器您只需要一个实例。

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

这意味着层次结构中只包含一个A“实例”。因此

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

希望这有助于作为迷你总结。有关详细信息,请阅读thisthis。还有一个很好的例子here

答案 1 :(得分:233)

关于内存布局

作为旁注,Dreaded Diamond的问题是基类存在多次。因此,通过常规继承,您相信您拥有:

  A
 / \
B   C
 \ /
  D

但是在内存布局中,你有:

A   A
|   |
B   C
 \ /
  D

这解释了为什么在致电D::foo()时,你有一个歧义问题。但是当您想要使用A的成员变量时,会出现真正的问题。例如,假设我们有:

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

当您尝试从m_iValue访问D时,编译器会抗议,因为在层次结构中,它会看到两个m_iValue,而不是一个B::m_iValue。如果您修改一个,例如A::m_iValueB的父C::m_iValue},则A::m_iValue将不会被修改(即C foo()的父母。

这就是虚拟继承的便利之处,就像它一样,你将回到真正的钻石布局,不仅有一个m_iValue方法,还有一个AB

可能出现什么问题?

想象:

  • C有一些基本功能。
  • m_iValue为它添加了一些很酷的数据(例如)
  • D添加了一些很酷的功能,例如观察者模式(例如,在B上)。
  • C继承自Am_iValue,因此来自D

使用正常继承,从m_iValues修改D是不明确的,必须解决此问题。即使是这样,m_iValue内也有两个D,所以你最好记住它并同时更新这两个。

使用虚拟继承,从D修改C是可以的......但是......假设你有B。通过其m_iValue界面,您附加了一个观察者。通过其m_iValue界面,您可以更新酷阵列,其副作用是直接更改C ...

由于直接完成C的更改(不使用虚拟访问器方法),将不会调用通过B“监听”的观察者,因为实现侦听的代码位于{ {1}},{{1}}并不知道...

结论

如果您的层次结构中有钻石,则表示您有95%的人对所述层次结构做错了。

答案 2 :(得分:32)

使用虚拟基础解释多重继承需要了解C ++对象模型。并且清楚地解释该主题最好在文章中而不是在评论框中完成。

我发现最好的,可读的解释解决了我对这个主题的所有怀疑是这篇文章:http://www.phpcompiler.org/articles/virtualinheritance.html

在阅读完之后,您真的不需要阅读有关该主题的任何其他内容(除非您是编译器编写者)...

答案 3 :(得分:9)

  

虚拟基类是一个类   无法实例化:你不能   从中创造直接对象。

我认为你混淆了两件截然不同的事情。虚拟继承与抽象类不同。虚拟继承修改函数调用的行为;有时候它会解析函数调用,否则这些调用是模糊的,有时它会将函数调用处理推迟到非虚拟继承所期望的类之外。

答案 4 :(得分:6)

我想补充OJ的善意澄清。

虚拟继承并非没有代价。就像所有虚拟事物一样,你会受到性能影响。这种性能打击有一种方法可能不太优雅。

不是通过虚拟衍生来破坏钻石,而是可以在钻石中添加另一层,以获得类似的结果:

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

所有类都没有虚拟继承,所有类都是公开继承的。然后,类D21和D22将隐藏虚拟函数f(),这对于DD来说是不明确的,可能通过将该函数声明为私有。它们分别定义了一个包装函数f1()和f2(),每个函数调用class-local(private)f(),从而解决冲突。 DD类如果需要D11 :: f()则调用f1(),如果需要D12 :: f()则调用f2()。如果你内联定义包装器,你可能会得到零开销。

当然,如果您可以更改D11和D12,那么您可以在这些类中执行相同的操作,但通常情况并非如此。

答案 5 :(得分:4)

除了关于多重和虚拟继承的内容之外,还有一篇关于Dobb博士期刊的非常有趣的文章:Multiple Inheritance Considered Useful

答案 6 :(得分:1)

这意味着对虚拟函数的调用将被转发到“正确”类。

C ++ FAQ Lite FTW。

简而言之,它通常用于多继承场景,其中形成“菱形”层次结构。当您在该类中调用函数并且该函数需要被解析为该底层类之上的类D1或D2时,虚拟继承将打破底层中创建的歧义。有关图表和详细信息,请参阅FAQ item

它也用于姐妹代表团,这是一个强大的功能(虽然不适合胆小的人)。请参阅this常见问题解答。

另见有效C ++第3版(第2版第43版)中的第40项。

答案 7 :(得分:1)

你有点困惑。我不知道你是否在混淆一些概念。

您的OP中没有虚拟基类。你只有一个基类。

你做了虚拟继承。这通常用于多继承,以便多个派生类使用基类的成员而不再生成它们。

不实例化具有纯虚函数的基类。这需要Paul得到的语法。通常使用它,以便派生类必须定义这些函数。

我不想再解释这个问题因为我没有完全得到你所要求的。

答案 8 :(得分:1)

钻石继承可运行的使用示例

此示例显示如何在典型场景中使用虚拟基类:解决钻石继承。

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

答案 9 :(得分:1)

常规继承

对于典型的3级非钻石非虚拟继承,当实例化一个新的最派生对象时,将调用new,并且由编译器从类类型解析该对象所需的大小,并将其传递给新的。

new具有签名:

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

并调用malloc,返回空指针

然后将其传递给派生程度最高的对象的构造函数,该对象将立即调用中间构造函数,然后中间构造函数将立即调用基本构造函数。然后,基础在对象的开头存储指向其虚拟表的指针,然后在其之后存储指向其属性的指针。然后返回到中间构造函数,该构造函数会将其虚拟表指针存储在同一位置,然后将其属性存储在基本构造函数将存储的属性之后。它返回到最派生的构造函数,该构造函数将指针存储在相同位置的虚拟表,然后将其属性存储在中间构造函数将存储的属性之后。

由于虚拟表指针被覆盖,因此虚拟表指针最终总是最派生的类之一。虚拟性向最派生的类传播,因此,如果一个函数在中产类中是虚拟的,那么它将在最派生类中而不是基类中是虚拟的。如果将大多数派生类的实例多态转换为指向基类的指针,则编译器不会将其解析为对虚拟表的间接调用,而是直接调用函数A::function()。如果函数是针对您所转换类型的虚拟函数,则它将解析为对虚拟表的调用,该调用始终是最派生类的调用。如果该类型不是虚拟的,则它将调用Type::function()并将对象指针传递给它,然后转换为Type。

实际上,当我说到指向其虚拟表的指针时,它实际上总是在虚拟表中偏移16。

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在派生程度较低的类中是虚拟的,则在派生程度较高的类中不再需要它,因为它会传播。但这可以用来表明该函数确实是一个虚拟函数,而不必检查它继承的类型定义的类。

override是另一个编译器防护,它表示此函数将覆盖某些内容,如果没有覆盖,则会引发编译器错误。

= 0表示这是一个抽象函数

final阻止在更多派生类中再次实现虚函数,并确保最派​​生类的虚拟表包含该类的最终函数。

= default在文档中明确指出编译器将使用默认实现

= delete如果尝试调用

,则会给出编译器错误

虚拟继承

考虑

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

实际上没有继承低音类,您将得到一个看起来像这样的对象:

代替此:

即将有2个基础对象。

在上述虚拟钻石继承情况下,在调用new之后,它将调用派生程度最高的构造函数,并且在该构造函数中,它将调用将3个偏移量传递到其虚拟表中的所有3个派生构造函数,而不是仅调用{{1} }和DerivedClass1::DerivedClass1(),然后都调用DerivedClass2::DerivedClass2()

以下内容都是在调试模式-O0下编译的,因此会有多余的程序集

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

它使用指向对象偏移量32的指针调用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 。Base将指向其虚拟表的指针存储在其接收的地址及其后的成员上。

Base::Base()

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 然后使用指向对象偏移量0的指针调用DerivedDerivedClass::DerivedDerivedClass(),并传递地址DerivedClass1::DerivedClass1()

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() 将对象+16的地址和DerivedDerivedClass::DerivedDerivedClass()的VTT地址传递给DerivedDerivedClass+24,其汇编程序与DerivedClass2::DerivedClass2()相同,除了行{{ 1}}对于DerivedClass1::DerivedClass1()显然是4而不是3。

此后,它将mov DWORD PTR [rax+8], 3的vtable中指向该类表示形式的偏移量的指针替换为对象中的所有3个虚拟表指针。

d = 4

DerivedDerivedClass

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

答案 10 :(得分:0)

虚拟类与虚拟继承相同。无法实例化的虚拟类,虚拟继承完全是另一回事。

维基百科对它的描述比我能做得更好。 http://en.wikipedia.org/wiki/Virtual_inheritance