我刚刚在C ++ FAQ Lite中阅读了这篇文章
[25.10]通过虚拟继承“委托给姐妹班”是什么意思?
class Base {
public:
virtual void foo() = 0;
virtual void bar() = 0;
};
class Der1 : public virtual Base {
public:
virtual void foo();
};
void Der1::foo()
{ bar(); }
class Der2 : public virtual Base {
public:
virtual void bar();
};
class Join : public Der1, public Der2 {
public:
...
};
int main()
{
Join* p1 = new Join();
Der1* p2 = p1;
Base* p3 = p1;
p1->foo();
p2->foo();
p3->foo();
}
“信不信由你,当Der1 :: foo()调用this-> bar()时,它最终调用Der2 :: bar()。是的,这是正确的:一个Der1对提供什么一无所知的类Der1 :: foo()调用的虚函数的重写。这种“交叉委托”可以是一种用于自定义多态类行为的强大技术。“
我的问题是:
幕后发生了什么。
如果我添加Der3(虚拟继承自Base),会发生什么? (我这里没有编译器,现在无法测试它。)
答案 0 :(得分:6)
幕后发生的事情。
简单的解释是,因为Base
的继承在Der1
和Der2
中都是虚拟的,所以在派生对象{{1}中有一个对象的实例}。在编译时,假设(这是常见的情况)虚拟表作为调度机制,在编译Join
时,它会通过vtable将调用重定向到Der1::foo
。
现在问题是编译器如何为每个对象生成vtable,bar()
的vtable将包含两个空指针,Base
的vtable将包含Der1
和null指针和Der1::foo
的vtable将包含空指针和Der2
[*]
现在,由于前一级别的虚拟继承,当编译器处理Der2::bar
时,它将创建一个Join
对象,因此Base
的{{1}}子对象的单个vtable {1}}。它有效地合并了Base
和Join
的vtable,并生成了一个vtable,其中包含指向Der1
和Der2
的指针。
因此Der1::foo
中的代码将通过Der2::bar
的vtable分发到最终的覆盖器,在这种情况下,它位于虚拟继承层次结构的不同分支中。
如果添加一个Der1::foo
类,并且该类定义了其中一个虚函数,编译器将无法干净地合并三个vtable并且会抱怨,并且会出现与multiply的模糊性有关的一些错误定义的方法(没有一个覆盖器可以被认为是最终覆盖器)。如果向Join
添加相同的方法,则歧义将不再是问题,因为最终的覆盖将是Der3
中定义的成员函数,因此编译器能够生成虚拟表
[*]大多数编译器不会在这里写空指针,而是指向通用函数的指针,该函数将打印错误消息并Join
应用程序,从而允许比普通分段错误更好的诊断。
答案 1 :(得分:1)
如果添加Der3
将会发生什么,取决于它从哪个类继承。
如您所知,只有在定义了所有虚函数后才能实例化类;否则你只能指向它们。这是为了防止构建部分定义的对象。
在您的示例中,您无法直接实例化Der1
或Der2
,因为在Der1
中,bar()
仍然是纯虚拟的Der2
,{{1}是纯虚的。
您的foo()
类可以实例化,因为它继承自两者,因此没有纯虚函数。
一旦创建了类的实例,就可以通过dynamic_casting实例化指向不可实例化的类的指针。
从实例化类的那一刻起,虚函数机制(使用指向函数的指针表)仍然会调用在实例化时定义的函数。
因此,关键是在创建对象时,创建Join
的实例。它的虚函数已定义,因为您可以创建对象。从那一刻起,您可以使用指向基类的任何指针调用虚函数。
答案 2 :(得分:0)
我明白为什么探索这个很有意思。在实际代码中,这可能几乎没有用。正如其他人所指出的那样,虚拟继承更像是一个修复 - 这个 - 设计 - 工作 - 某种方式工具,而不是一个有效的设计工具。
您的代码在VS2010中产生警告 - 编译器让您知道正在使用优势。当然,这不是一个显示阻止,但另一个灰心使用它。
如果你像这样介绍Der3
class Der3 : public virtual Base {
public:
void bar() {}
};
class Join : public Der1, public Der2, public Der3 {}
由于ambiguous inheritance of 'void Base::bar(void)'
,代码无法编译
答案 3 :(得分:0)
讨论中缺少一点(尽管如此,这是非常有用的,并且感谢所有人)。 当你真正继承'一个班级。会发生什么:大多数编译器都保留了指向虚拟基类的指针(它可以由不同的编译器以不同的方式实现)。因此,如果你采用Der1和Der2的大小,它将是32位上的至少4个字节和64位上的8个字节。因为它们有一个指向虚拟基类的指针,因此没有歧义。这就是为什么当你创建Join的对象时,它首先调用Virtual Base类的构造函数(实际上不是第一次调用,但它初始化通过Der1和Der2首先在其construtor中来到它的指针)。在Join编译器中可以检查指针名称/类型,然后确保只有一个虚拟基类的指针来自Der1和Der2。您甚至可以通过sizeof运算符检查。我们知道编译器会默默地将调用放入构造函数中。因此,它首先以Depth First方式调用Virtual Base类的构造函数。 (可以使用所有基类作为虚拟派生来检查)。休息已经解释
答案 4 :(得分:-1)
这是一个非常愚蠢的例子imo,是学术界让自己看起来聪明的完美典范。如果出现这种情况,那几乎可能是因为一个错误,特别是忘记让Der1 :: foo()虚拟。
修改:
我误读了类定义。这正是这种设计的问题。需要花费很多心思来确定每种情况下会发生什么,这很糟糕。使你的代码可读比远远好于像这样“聪明”。