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。如果我们进行虚拟继承,即在标有 (*) 的行中添加virtualbefore ,它会打印.public4225

  1. 你能解释一下为什么1会改变4吗?
  2. 你能解释一下static_cast<D2&>(m)和的意思static_cast<B*>(d1)吗?
  3. 你怎么不会迷失在这种组合中?你在画什么吗?
  4. 在正常项目中发现如此复杂的设置是否很常见?
4

5 回答 5

5

图片胜于雄辩,所以在答案之前...


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

    M
   / \
  D1 D2
  |   |
  B   B

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

    M
   / \
  D1 D2
   \ /
    B

  1. Cross-Delegation,或者我喜欢称之为的兄弟多态性。虚拟基继承会将 B::f() 覆盖修复为 D2:f()。希望当您考虑在何处实现虚函数以及它们作为继承链的结果覆盖什么时,图片有助于解释这一点。

  2. 在这种情况下,static_cast运算符的使用推动了从派生类到基类类型的转换。

  3. 阅读非常糟糕的代码并了解该语言的基础如何“工作”的大量经验

  4. 谢天谢地没有。这并不常见。但是,如果这完全令人困惑,那么原始的 iostream 库会让您做噩梦。

于 2012-11-11T17:45:39.433 回答
4

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

为什么会变成4? 因为交叉授权

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

B   B
|   |
D1  D2
 \ /
  M

d1是 a D1,所以它D2甚至不知道存在,它的父 ( B) 也不知道D2存在。唯一可能的结果是B::f()被调用。

添加虚拟继承后,基类合并在一起。

  B
 / \
D1  D2
 \ /
  M

在这里,当您请求d1f(),它会查看其父级。现在,他们共享相同的B,所以B'sf()将被覆盖D2::f(),你得到4.

是的,这很奇怪,因为这意味着它D1已经设法从 调用了一个函数D2,而它对此一无所知。这是 C++ 中比较奇怪的部分之一,通常会被避免。


你能解释一下 static_cast(m) 和 static_cast(d1) 的含义吗?

你不明白什么?他们分别施放md1D2&B*


你怎么不会迷失在这种组合中?你在画什么吗?

在这种情况下不是。它很复杂,但足够小,可以留在你的脑海中。我在上面的示例中绘制了图表,以使事情尽可能清晰。


在正常项目中发现如此复杂的设置是否很常见?

不。每个人都知道要避免可怕的菱形继承模式,因为它太复杂了,而且通常有一种更简单的方法可以做任何你想做的事情。

一般来说,最好是组合而不是继承

于 2012-11-11T17:41:40.197 回答
2

这个问题实际上是多个问题:

  1. 为什么使用非继承时virtual函数没有被覆盖?答案当然是你有两个对象:一个是覆盖的基础,另一个是不覆盖的基础。根据您在调用时认为您的对象从哪个分支派生,您将得到不同的结果。当您将设置更改为只有一个子对象时,将考虑继承图中的任何覆盖(如果两个分支都覆盖它,我认为除非您在分支再次合并的地方覆盖它,否则您会收到错误消息。B::f()virtualBaseD1f()D2f()f()B
  2. 是什么static_cast<D2&>(m)意思?f()由于来自 的有两个版本Base,因此您需要选择您想要的一个。与static_cast<D2&>(m)您一起将其M视为一个D2对象。如果没有强制转换,编译器将无法判断您正在查看的两个主题中的哪一个,并且会产生歧义错误。
  3. 是什么static_cast<B*>(d1)意思?它恰好是不必要的,但仅将对象视为B*对象。

一般来说,我倾向于避免对任何不重要的事情进行多重继承。大多数时候,我使用多重继承来利用空基优化或创建具有可变数量成员的东西(想想std::tuple<...>)。我不确定我是否真的需要使用多重继承来处理生产代码中的多态性。

于 2012-11-11T17:40:44.060 回答
2

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

如果没有虚拟继承,则有两个in实例,一个用于这个“菱形”的每个分支。菱形边之一 ( ) 会覆盖该函数,而另一个 ( ) 则不会。由于被声明为,意味着您希望访问其函数未被覆盖的副本。如果你要投到,你会得到不同的结果。BMD2D1d1D1d1->f()BD2

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

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

他们分别投到D2&B*。由于g不是虚拟的,因此B:::g被调用。

3)你怎么不会迷失在这种组合中?你在画什么吗?

有时 ;)

4)在正常项目中发现如此复杂的设置是否常见?

不太常见。事实上,没有多重更不用说虚拟继承(Java,C#...),所有语言都可以很好地运行。

但是,在某些情况下它可以使事情变得更容易,尤其是在库开发中。

于 2012-11-11T17:55:12.483 回答
2

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

没有virtual继承,有2个独立的继承层次;B->D1->MB->D2->M。所以想象2个virtual函数表(尽管这是实现定义的)。
当您调用f()with 时D1*,它只知道关于,B::f()此而已。通过virtual继承,baseclass B被委托给M并因此D2::f()被视为class M.

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

static_cast<D2&>(m), 就像考虑 as 的对象,class M就像考虑asclass D2
static_cast<B*>(d1)的指针。 两者都是有效的演员表。 因为不是函数选择发生在compile-time。如果是这样,那么所有这些选角都无关紧要。class D1class B1

g()virtualvirtual

(3) 你怎么不会迷失在这种组合中?你在画什么吗?

当然它很复杂,乍一看,如果有这么多这样的类,很容易迷路。

(4)在正常项目中是否经常发现这种复杂的设置?

一点也不,这是不寻常的,有时是代码的味道。

于 2012-11-11T17:50:40.357 回答