多重继承和多态问题

11

考虑以下这段 C++ 代码:

#include <iostream>
using namespace std;

struct B {
    virtual int f() { return 1; }
    int g() { return 2; }
};
struct D1 : public B { // (*)
    int g() { return 3; }
};
struct D2 : public B { // (*)
    virtual int f() { return 4; }
};
struct M : public D1, public D2 {
    int g() { return 5; }
};

int main() {
    M m;
    D1* d1 = &m;
    cout << d1->f()
         << static_cast<D2&>(m).g()
         << static_cast<B*>(d1)->g()
         << m.g();
}

它会打印出1225。如果我们使用虚继承,即在标有(*)的行中在public之前添加virtual,则会打印4225

  1. 能否解释一下为什么1变成了4
  2. 能否解释一下static_cast<D2&>(m)static_cast<B*>(d1)的意义?
  3. 你如何避免在这种组合中迷失方向?你画图了吗?
  4. 正常项目中是否常见遇到这样复杂的设置?
5个回答

5

图片胜过千言万语,因此在回答之前...


对于D1和D2没有B的虚拟基类继承的M层次结构:

    M
   / \
  D1 D2
  |   |
  B   B

使用虚基类继承的B对D1和D2进行M类层次结构:

    M
   / \
  D1 D2
   \ /
    B

  1. 交叉委托,或者像我一样称之为带有变化的兄弟多态性。虚基类继承将修复B::f()重写为D2:f()。当您考虑虚函数的实现位置以及由于继承链而覆盖的内容时,希望这张图片有助于解释。

  2. 在这种情况下,static_cast运算符用于从派生类转换为基类类型。

  3. 阅读非常糟糕的代码并了解语言的底层原理的经验。

  4. 幸运的是不常见。不过,如果您对iostream库感到困惑,原始iostream库可能会让您噩梦连连。


4

你能解释一下为什么1会变成4吗?

为什么会变成4呢?因为涉及到交叉委托

这是在虚继承之前的继承图:

B   B
|   |
D1  D2
 \ /
  M
is a , so it has no idea that even exists, and its parent () has no idea that exists. The only possible result is that is called.

After virtual inheritance is added, the base classes are merged together.

  B
 / \
D1  D2
 \ /
  M

这里,当你请求 f() 时,d1 会寻找它的父级。现在,它们共享同一个 B,所以 Bf() 将被 D2::f() 覆盖,得到的结果是 4
是的,这很奇怪,因为这意味着 D1 成功调用了来自于它一无所知的 D2 的函数。这是 C++ 中更奇怪的部分之一,并且通常应该避免使用。

你能解释一下 static_cast(m) 和 static_cast(d1) 的意思吗?

你不理解什么?它们将 md1 分别转换为 D2&B*


你在这种组合中如何不迷路?你是在画图吗?

并非如此。虽然很复杂,但规模小到足以记在脑海中。我已经绘制了上面示例中的图形,以尽可能清晰地表达。


在普通项目中出现这样复杂的设置常见吗?

不常见。每个人都知道要避免可怕的继承钻石模式,因为它太复杂了,通常有更简单的方法来实现你想做的事情。

一般来说,优先使用组合而非继承更好。


2
这个问题实际上是多个问题:
1. 当使用非虚拟继承时,为什么虚拟函数B::f()没有被覆盖?答案当然是你有两个Base对象:一个作为D1的基类覆盖了f(),另一个作为D2的基类没有覆盖f()。根据你从哪个分支派生对象调用f(),你会得到不同的结果。当你改变设置只有一个B子对象时,继承图中的任何覆盖都会被考虑(如果两个分支都覆盖它,我认为你会得到一个错误,除非你在分支再次合并的地方覆盖它)。
2. static_cast(m)是什么意思?由于有两个来自Base的f()版本,你需要选择你想要的版本。使用static_cast(m),你将M视为D2对象。如果没有强制转换,编译器将无法确定你正在查看哪个对象中的两个主题,并且会产生模棱两可的错误。
3. static_cast(d1)是什么意思?这是不必要的,但它只将对象视为B*对象。
通常,我倾向于避免非平凡情况下的多重继承。大多数时候,我使用多重继承来利用空基类优化或创建具有可变成员数量的东西(比如std::tuple<...>)。我不确定我是否曾经遇到过在生产代码中使用多重继承处理多态性的实际需要。

2
(1) 你能解释一下为什么1会变成4吗?没有虚拟继承,有两个独立的继承层次结构;B->D1->M和B->D2->M。所以想象两个虚拟函数表(尽管这是实现定义)。当你使用D1*调用f()时,它只知道B::f(),就是这样。使用虚拟继承,基类B被委派给M,因此D2::f()被视为M类的一部分。
(2) 你能解释一下static_cast(m)和static_cast(d1)的含义吗?static_cast(m)就像将class M的对象视为class D2一样。static_cast(d1)就像将class D1的指针视为class B1一样。两者都是有效的转换。由于g()不是虚拟的,函数选择发生在编译时。如果它是虚拟的,那么所有这些转换都无关紧要。
(3) 你怎么不会在这种组合中迷失方向?你画了些什么吗?当然,这很复杂,在第一眼看到有这么多这样的类时,人们可能很容易迷失方向。
(4) 在普通项目中发现这样复杂的设置常见吗?一点也不,这是不寻常的,有时候会是代码异味。

1
我接受了这个答案,因为它对所有要点都有很好的解释,特别是第二点。然而,这是一个艰难的决定,因为其他答案也很棒!非常感谢大家! - Adam Stelmaszczyk
@AdamStelmaszczyk,关于第二点的额外信息。调用所需的g()函数的更好方法是删除static_cast,并分别使用m.D2::g()d1->B::g() - iammilind

2

1) 你能解释一下为什么1会变成4吗?

如果没有虚继承,在“菱形”结构中的每个分支中都会有一个B的实例,因此在M中有两个实例。其中一个菱形边缘(D2)覆盖了函数,而另一个(D1)则没有。由于d1声明为D1,所以d1->f()表示您希望访问未被覆盖的B的副本。如果您将其转换为D2,则会得到不同的结果。

通过使用虚继承,您将B的两个实例合并为一个,因此一旦创建了MD2::f就有效地覆盖了B:f

2) 你能解释一下static_cast<D2&>(m)static_cast<B*>(d1)的含义吗?

它们分别转换为D2&B*。由于g不是虚函数,因此调用B:::g

3) 你是如何不迷失在这种组合中的?你画了什么吗?

有时候会画一些图;)

4) 在普通项目中发现这样复杂的设置很常见吗?

并不太常见。实际上,有些语言甚至没有多重继承,更不用说虚继承了(例如Java、C#等)。

然而,在库开发方面,有时使用虚继承可以使事情变得更加容易。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接