问题 多重继承和多态问题


考虑这个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。如果我们进行虚拟继承,即添加 virtual 之前 public 在标有(*)的行中,它会打印出来 4225

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

9433
2017-11-11 17:21


起源



答案:


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

没有 virtual 继承,有2个独立的继承层次结构; B->D1->M 和 B->D2->M。所以想象2 virtual 函数表(虽然这是实现定义的)。
当你调用时 f() 同 D1*,它 只是 知道 B::f() 就是这样。同 virtual 继承,基础 class B 被委派给 M 因此 D2::f() 被视为的一部分 class M

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

static_cast<D2&>(m),就像考虑对象一样 class M 如 class D2
static_cast<B*>(d1),就像考虑指针一样 class D1 如 class B1
两者都是有效的演员表。
以来 g() 不是 virtual 功能选择发生在 编译时间。如果是的话 virtual 然后所有这些铸造都无所谓。

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

当然,它很复杂,乍看之下,如果有这么多类,可能很容易迷失。

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

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


2
2017-11-11 17:50



我接受了这个答案,因为对所有要点都有很好的解释,特别是2.然而,这是很难做出的决定,因为其他答案也很棒!非常感谢你们! - Adam Stelmaszczyk
@AdamStelmaszczyk,关于第2点的其他信息。更好的方式来调用所需的 g() 功能是由 去除 该 static_casts并简单地用作: m.D2::g() 和 d1->B::g() 分别。 - iammilind


答案:


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

没有 virtual 继承,有2个独立的继承层次结构; B->D1->M 和 B->D2->M。所以想象2 virtual 函数表(虽然这是实现定义的)。
当你调用时 f() 同 D1*,它 只是 知道 B::f() 就是这样。同 virtual 继承,基础 class B 被委派给 M 因此 D2::f() 被视为的一部分 class M

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

static_cast<D2&>(m),就像考虑对象一样 class M 如 class D2
static_cast<B*>(d1),就像考虑指针一样 class D1 如 class B1
两者都是有效的演员表。
以来 g() 不是 virtual 功能选择发生在 编译时间。如果是的话 virtual 然后所有这些铸造都无所谓。

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

当然,它很复杂,乍看之下,如果有这么多类,可能很容易迷失。

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

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


2
2017-11-11 17:50



我接受了这个答案,因为对所有要点都有很好的解释,特别是2.然而,这是很难做出的决定,因为其他答案也很棒!非常感谢你们! - Adam Stelmaszczyk
@AdamStelmaszczyk,关于第2点的其他信息。更好的方式来调用所需的 g() 功能是由 去除 该 static_casts并简单地用作: m.D2::g() 和 d1->B::g() 分别。 - iammilind


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


M级层次结构 WITHOUT D1和D2的B的虚拟基础继承:

    M
   / \
  D1 D2
  |   |
  B   B

M级层次结构 WITH D1和D2的B的虚拟基础继承:

    M
   / \
  D1 D2
   \ /
    B

  1. 跨代表团,或者我喜欢称它为兄弟 - 多态与扭曲。虚基础继承将B :: f()重写修复为D2:f()。希望图片在您考虑虚拟函数的实现位置以及它们因继承链而重写的内容时有助于解释这一点。

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

  3. 很多经验,读取非常糟糕的代码,并了解语言“工作”的基础

  4. 谢天谢地。这并不常见。然而,原来的iostream库会给你带来噩梦,如果这总是令人困惑的话。


5
2017-11-11 17:45





你能解释为什么1改为4吗?

为什么会变为 4?因为 跨代表团

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

B   B
|   |
D1  D2
 \ /
  M

d1 是一个 D1,所以它不知道 D2 甚至存在,及其父母(B)不知道 D2 存在。唯一可能的结果是 B::f() 叫做。

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

  B
 / \
D1  D2
 \ /
  M

在这里,当你问 d1对于 f(),它看起来是它的父母。现在,他们也有同感 B所以 Bf() 将被覆盖 D2::f() 你明白了 4

是的,这很奇怪,因为它意味着 D1 已设法调用函数 D2,这一点都不知道。这是C ++中较奇怪的部分之一,通常可以避免。


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

你不明白什么?他们演员 m 和 d1 至 D2& 和 B* 分别。


你怎么不迷失在这种组合中?你在画东西吗?

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


在普通项目中发现这种复杂的设置是否常见?

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

一般来说,它更好 更喜欢构成而不是继承


4
2017-11-11 17:41





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

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

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


2
2017-11-11 17:40





1)你能解释为什么1改为4吗?

没有虚拟继承,就有 两个例子 的 B 在 M,这个“钻石”的每个分支一个。其中一个钻石边缘(D2)覆盖函数和其他(D1)没有。以来 d1 被宣布为 D1d1->f() 意味着你想要访问的副本 B 其功能是  覆盖。如果你要施展 D2,你会得到不同的结果。

通过使用虚拟继承,您可以合并两个实例 B 合而为一 D2::f 有效地覆盖 B:f 一旦 M 是。

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

他们投了 D2& 和 B* 分别。以来 g 不是虚拟的, B:::g 被叫。

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

有时候;)

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

不太常见。事实上,完全没有多个简单的虚拟继承(Java,C#...)就可以获得完整的语言。

但是,有时它可以使事情变得更容易,特别是在图书馆开发中。


2
2017-11-11 17:55