要打电话的功能? (委托姐妹班)

时间:2010-12-20 08:19:22

标签: c++

我刚刚在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()调用的虚函数的重写。这种“交叉委托”可以是一种用于自定义多态类行为的强大技术。“

我的问题是:

  1. 幕后发生了什么。

  2. 如果我添加Der3(虚拟继承自Base),会发生什么? (我这里没有编译器,现在无法测试它。)

5 个答案:

答案 0 :(得分:6)

  

幕后发生的事情。

简单的解释是,因为Base的继承在Der1Der2中都是虚拟的,所以在派生对象{{1}中有一个对象的实例}。在编译时,假设(这是常见的情况)虚拟表作为调度机制,在编译Join时,它会通过vtable将调用重定向到Der1::foo

现在问题是编译器如何为每个对象生成vtable,bar()的vtable将包含两个空指针,Base的vtable将包含Der1和null指针和Der1::foo的vtable将包含空指针和Der2 [*]

现在,由于前一级别的虚拟继承,当编译器处理Der2::bar时,它将创建一个Join对象,因此Base的{​​{1}}子对象的单个vtable {1}}。它有效地合并了BaseJoin的vtable,并生成了一个vtable,其中包含指向Der1Der2的指针。

因此Der1::foo中的代码将通过Der2::bar的vtable分发到最终的覆盖器,在这种情况下,它位于虚拟继承层次结构的不同分支中。

如果添加一个Der1::foo类,并且该类定义了其中一个虚函数,编译器将无法干净地合并三个vtable并且会抱怨,并且会出现与multiply的模糊性有关的一些错误定义的方法(没有一个覆盖器可以被认为是最终覆盖器)。如果向Join添加相同的方法,则歧义将不再是问题,因为最终的覆盖将是Der3中定义的成员函数,因此编译器能够生成虚拟表

[*]大多数编译器不会在这里写空指针,而是指向通用函数的指针,该函数将打印错误消息并Join应用程序,从而允许比普通分段错误更好的诊断。

答案 1 :(得分:1)

如果添加Der3将会发生什么,取决于它从哪个类继承。

如您所知,只有在定义了所有虚函数后才能实例化类;否则你只能指向它们。这是为了防止构建部分定义的对象。

在您的示例中,您无法直接实例化Der1Der2,因为在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()虚拟。

修改

我误读了类定义。这正是这种设计的问题。需要花费很多心思来确定每种情况下会发生什么,这很糟糕。使你的代码可读比远远好于像这样“聪明”。