我想知道什么是“虚拟基类”以及它的含义。
让我举个例子:
class Foo
{
public:
void DoSomething() { /* ... */ }
};
class Bar : public virtual Foo
{
public:
void DoSpecific() { /* ... */ }
};
我想知道什么是“虚拟基类”以及它的含义。
让我举个例子:
class Foo
{
public:
void DoSomething() { /* ... */ }
};
class Bar : public virtual Foo
{
public:
void DoSpecific() { /* ... */ }
};
当前回答
解释使用虚拟基的多重继承需要c++对象模型的知识。最好是在一篇文章中,而不是在评论框中,清楚地解释这个主题。
我发现最好的、易读的解释是这篇文章,它解决了我对这个问题的所有疑问:http://www.phpcompiler.org/articles/virtualinheritance.html
在读完这篇文章之后,你真的不需要再阅读关于这个主题的任何其他内容了(除非你是编译器作者)…
其他回答
钻石继承可运行的用法示例
这个示例展示了如何在典型场景中使用虚拟基类:解决菱形继承问题。
考虑下面的工作示例:
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);
}
编译并运行:
g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out
如果我们移除virtual into:
class B : public virtual A
我们会得到一堵关于GCC无法解析D成员和通过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
在GCC 9.3.0, Ubuntu 20.04上测试。
这意味着对虚函数的调用将被转发到“正确的”类。
C+ FAQ上升。
简而言之,它通常用于多继承场景,其中形成了“菱形”层次结构。虚继承将打破在底层类中创建的模糊性,当您调用该类中的函数并且函数需要解析到底层类之上的类D1或D2时。有关图表和详细信息,请参阅FAQ项。
它还用于姐妹委托,这是一个强大的功能(尽管不适合心脏薄弱的人)。请看这个FAQ。
参见Effective c++第3版中的第40项(第2版中的43项)。
我想对OJ的善意澄清补充一点。
虚拟继承是有代价的。就像所有虚拟的东西一样,你的性能会受到影响。有一种方法可以绕过这种性能冲击,但可能不那么优雅。
你可以在菱形中添加另一层,而不是通过虚拟派生来打破菱形,以得到如下内容:
B
/ \
D11 D12
| |
D21 D22
\ /
DD
没有一个类是虚继承的,所有类都是公开继承的。类D21和D22将隐藏虚函数f(),这对于DD来说是模糊的,可能是通过将函数声明为private。它们将分别定义一个包装器函数f1()和f2(),分别调用class-local (private) f(),从而解决冲突。类DD在需要D11::f()时调用f1(),在需要D12::f()时调用f2()。如果内联定义包装器,可能会得到零开销。
当然,如果您可以更改D11和D12,那么您可以在这些类中执行相同的技巧,但通常情况下并非如此。
关于内存布局
作为旁注,可怕的钻石的问题是基类出现了多次。通过常规遗传,你相信你有:
A
/ \
B C
\ /
D
但是在内存布局中,你有:
A A
| |
B C
\ /
D
这解释了为什么当调用D::foo()时,你有一个歧义问题。但真正的问题是当你想要使用a的成员变量时。例如,假设我们有:
class A
{
public :
foo() ;
int m_iValue ;
} ;
当你试图从D访问m_iValue时,编译器会抗议,因为在层次结构中,它会看到两个m_iValue,而不是一个。如果你修改了一个,比如B::m_iValue (B的A::m_iValue父元素),C::m_iValue将不会被修改(C的A::m_iValue父元素)。
这就是虚拟继承方便的地方,有了它,你会回到一个真正的菱形布局,不仅只有一个foo()方法,还有一个且只有一个m_iValue。
会出什么问题呢?
想象一下:
A有一些基本特征。 B添加了一些很酷的数据数组(例如) C给它添加了一些很酷的特性,比如观察者模式(例如,在m_iValue上)。 D继承了B和C,因此也继承了A。
对于普通继承,从D修改m_iValue是不明确的,必须解决这个问题。即使它是,在D中有两个m_iValues,所以你最好记住这一点,并同时更新这两个。
使用虚拟继承,从D修改m_iValue是可以的…但是…假设你有d,通过它的C接口,你连接了一个观察者。通过它的B接口,你更新了酷数组,这有直接改变m_iValue的副作用…
由于m_iValue的改变是直接完成的(不使用虚拟访问方法),通过C“监听”的观察者将不会被调用,因为实现监听的代码是在C中,而B不知道它…
结论
如果你的层次结构中有一颗钻石,这意味着你有95%的概率在该层次结构中做了错事。
虚拟基类用于虚拟继承,是一种使用多重继承时防止在继承层次结构中出现给定类的多个“实例”的方法。
考虑以下场景:
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和C组成,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
这是一个小总结。要了解更多信息,请阅读这个和这个。这里还有一个很好的例子。