为什么我应该避免C ++中的多重继承?

问题描述 投票:155回答:15

使用多重继承是一个好主意还是我可以做其他事情?

c++ oop multiple-inheritance
15个回答
252
投票

多重继承(缩写为MI)闻起来,这意味着通常,它是出于不好的原因而完成的,它会在维护者面前反击。

摘要

  1. 考虑功能的组合,而不是继承
  2. 警惕恐惧钻石
  3. 考虑继承多个接口而不是对象
  4. 有时,多重继承是正确的。如果是,则使用它。
  5. 准备好在代码审查中捍卫您的多重继承架构

也许是作文?

这对于继承来说是正确的,因此,对于多重继承来说更是如此。

你的对象真的需要从另一个继承吗? Car不需要继承Engine工作,也不需要继承Wheel。一个Car有一个Engine和四个Wheel

如果你使用多重继承来解决这些问题而不是组合,那么你做错了。

2.恐惧钻石

通常,你有一个类A,然后BC都继承自A。而且(不要问我为什么)有人然后决定D必须继承BC

我在8个8年里遇到过这种问题两次,看到因为:

  1. 它从一开始就犯了多少错误(在这两种情况下,D不应该继承BC),因为这是糟糕的架构(事实上,C根本就不存在......)
  2. 有多少维护者为此付出了代价,因为在C ++中,父类A在其孙子类D中出现两次,因此,更新一个父字段A::field意味着更新它两次(通过B::fieldC::field),或者有一些东西是静默的错误和崩溃,后来(B::field新指针,删除C::field ......)

如果这不是你想要的,那么在C ++中使用关键字virtual来限定继承可以避免上面描述的双重布局,但无论如何,根据我的经验,你可能做错了......

在对象层次结构中,您应该尝试将层次结构保持为树(节点具有一个父节点),而不是图形。

More about the Diamond (edit 2017-05-03)

C ++中的恐惧钻石的真正问题(假设设计合理 - 让你的代码得到审查!),你需要做出选择:

  • A在你的版面中存在两次是否可取,这是什么意思?如果是,那么一定要从它继承两次。
  • 如果它只存在一次,那么实际上是从它继承而来的。

这个选择是问题所固有的,而且在C ++中,与其他语言不同,您实际上可以在没有教条的情况下执行此操作,从而在语言级别强制设计。

但是像所有权力一样,拥有这种权力的责任就在于:让您的设计得到审查

3.接口

零或一个具体类的多重继承,以及零个或多个接口通常是好的,因为您不会遇到上述的恐惧钻石。实际上,这就是用Java完成的事情。

通常,当C继承自AB时,你的意思是用户可以使用C,好像它是A,和/或好像它是B

在C ++中,接口是一个抽象类,它具有:

  1. 它的所有方法都声明为纯虚拟(后缀为= 0) (删除了2017-05-03)
  2. 没有成员变量

零到一个真实对象以及零个或多个接口的多重继承不被认为是“臭”(至少不是那么多)。

More about the C++ Abstract Interface (edit 2017-05-03)

首先,NVI模式可用于产生接口,因为真正的标准是没有状态(即没有成员变量,除了this)。你的抽象界面的意思是发布合同(“你可以用这种方式打电话给我,这样”),仅此而已。仅具有抽象虚拟方法的限制应该是设计选择,而不是义务。

其次,在C ++中,从抽象接口虚拟继承是有意义的(即使有额外的成本/间接)。如果不这样做,并且接口继承在层次结构中出现多次,那么您将有歧义。

第三,面向对象很棒,但它不是C ++中唯一的真理。使用正确的工具,并始终记住您在C ++中提供其他范例,提供不同类型的解决方案。

你真的需要多重继承吗?

有时候是。

通常,你的C类继承自ABAB是两个不相关的对象(即不在同一层次结构中,没有共同点,不同的概念等)。

例如,你可以有一个带有X,Y,Z坐标的Nodes系统,能够进行大量的几何计算(可能是一个点,几何对象的一部分),每个节点都是一个自动代理,能够与其他代理进行通信。

也许您已经可以访问两个库,每个库都有自己的命名空间(使用命名空间的另一个原因......但是你使用名称空间,不是吗?),一个是geo,另一个是ai

所以你有自己的own::Node来自ai::Agentgeo::Point

这是你应该问自己是否不应该使用构图的时刻。如果own::Node真的真的是ai::Agentgeo::Point,那么组合将不会。

然后你需要多重继承,让你的own::Node根据他们在3D空间中的位置与其他代理进行通信。

(你会注意到ai::Agentgeo::Point完全,完全,完全无关......这大大降低了多重继承的危险)

Other cases (edit 2017-05-03)

还有其他情况:

  • 使用(希望是私有的)继承作为实现细节
  • 一些像策略这样的C ++习语可以使用多重继承(当每个部分需要通过this与其他部分进行通信时)
  • 来自std :: exception的虚拟继承(Is Virtual Inheritance necessary for Exceptions?
  • 等等

有时你可以使用成分,有时MI更好。关键是:你有一个选择。负责任地做(并检查您的代码)。

那么,我应该做多重继承吗?

大部分时间,根据我的经验,没有。 MI不是正确的工具,即使它似乎有效,因为它可以被懒惰用于将特征结合在一起而不会意识到后果(比如使Car既是Engine又是Wheel)。

但有时候,是的。而在那个时候,没有比MI更好的了。

但是因为MI很臭,所以要准备好在代码审查中捍卫你的架构(并且保护它是一件好事,因为如果你无法保护它,那么你就不应该这样做)。


2
投票

你应该仔细使用它,有些情况下,如Diamond Problem,当事情变得复杂。

alt text (来源:learncpp.com


1
投票

Uses and Abuses of Inheritance.

这篇文章很好地解释了继承,这是危险的。


1
投票

除了菱形图案之外,多重继承往往会使对象模型更难理解,从而增加了维护成本。

组合本质上易于理解,理解和解释。编写代码可能会很繁琐,但是一个好的IDE(自从我使用Visual Studio已经过去几年,但肯定Java IDE都有很好的组合快捷方式自动化工具)应该让你克服这个障碍。

此外,在维护方面,“钻石问题”也出现在非文字继承实例中。例如,如果你有A和B,而你的C类扩展了它们,并且A有一个'makeJuice'方法制作橙汁,你可以扩展它以制作带有石灰味的橙汁:当设计师为' B'增加'makeJuice'方法产生电流? “A”和“B”现在可能与“父母”兼容,但这并不意味着他们将永远如此!

总的来说,倾向于避免继承,特别是多重继承的格言是合理的。正如所有格言,有例外,但你需要确保有一个闪烁的绿色霓虹灯标志指向你编码的任何例外(并训练你的大脑,以便你任何时候你看到你在自己闪烁的绿色霓虹灯绘制的继承树标志),并检查,以确保每隔一段时间都有意义。


1
投票

具体对象的MI的关键问题是,你很少有一个合法地应该“成为A并成为B”的对象,因此很少有逻辑上的正确解决方案。更常见的是,你有一个服从“C可以充当A或B”的对象C,你可以通过接口继承和组合来实现。但不要搞错 - 多个接口的继承仍然是MI,只是它的一个子集。

特别是对于C ++,该功能的关键弱点不是多重继承的实际存在,而是一些构造它允许几乎总是格式错误。例如,继承同一对象的多个副本,如:

class B : public A, public A {};

由于定义而不正确。翻译成英文,这是“B是A和A”。因此,即使在人类语言中,也存在严重的模糊性。你是说“B有2个”还是“B是A”?允许这样的病态代码,更糟糕的是使它成为一个使用示例,在提出将该功能保留在后继语言中时,C ++没有任何好处。


0
投票

您可以使用组合优先于继承。

一般的感觉是构图更好,而且讨论得很好。


0
投票

每个类需要4/8个字节。 (每个类一个这个指针)。

这可能永远不会成为一个问题,但如果有一天你有一个微数据结构,它将实现数十亿的时间。


140
投票

来自interview with Bjarne Stroustrup

人们非常正确地说你不需要多重继承,因为你可以用多重继承做任何事情,你也可以用单继承做。你只需使用我提到的委托技巧。此外,您根本不需要任何继承,因为您使用单继承执行的任何操作,您也可以通过转发类来继承。实际上,您也不需要任何类,因为您可以使用指针和数据结构来完成所有操作。但是你为什么要那样做呢?什么时候使用语言设施方便?你什么时候想要一个解决方法?我已经看到了多继承很有用的情况,我甚至看到过很复杂的多继承很有用的情况。通常,我更喜欢使用该语言提供的功能来进行变通


38
投票

没有理由避免它,它在情况下非常有用。但您需要了解潜在的问题。

最大的一个是死亡钻石:

class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;

您现在在Child中有两个GrandParent“副本”。

C ++已经考虑到了这一点,并允许您进行虚拟继承以解决问题。

class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;

始终检查您的设计,确保您没有使用继承来节省数据重用。如果你可以用组合表示相同的东西(通常你可以),这是一个更好的方法。


11
投票

见w:Multiple Inheritance

多重继承受到了批评,因此没有用多种语言实现。批评包括:

  • 复杂性增加
  • 语义模糊通常概括为diamond problem
  • 无法从单个类中显式继承多次
  • 继承顺序改变类语义。

使用C ++ / Java样式构造函数的语言中的多重继承会加剧构造函数和构造函数链接的继承问题,从而在这些语言中创建维护和可扩展性问题。在构造函数链接范例下,很难实现具有极大变化的构造方法的继承关系中的对象。

现代的解决方法是使用COM和Java接口等接口(纯抽象类)。

我可以用其他东西代替这个吗?

是的你可以。我要从GoF偷。

  • 编程到接口,而不是实现
  • 更喜欢继承的组合

8
投票

公共继承是一种IS-A关系,有时一个类将是几个不同类的类型,有时反映这一点很重要。

“Mixins”有时也很有用。它们通常是小类,通常不会从任何东西继承,提供有用的功能。

只要继承层次结构相当浅(因为它应该几乎总是如此),并且管理得当,您就不可能获得可怕的钻石继承。钻石对于使用多重继承的所有语言来说都不是问题,但是C ++对它的处理经常是尴尬的,有时令人费解。

虽然我遇到了多重继承非常方便的情况,但它们实际上相当罕见。这可能是因为当我不需要多重继承时,我更喜欢使用其他设计方法。我更愿意避免混淆语言结构,并且很容易构建继承案例,你必须非常好地阅读手册以弄清楚发生了什么。


6
投票

你不应该“避免”多重继承,但你应该意识到可能出现的问题,例如“钻石问题”(http://en.wikipedia.org/wiki/Diamond_problem),并小心对待给予你的力量,就像你应该拥有所有权力一样。


2
投票

每种编程语言对面向对象编程的处理都略有不同,有利有弊。 C ++的版本将重点放在了性能上,并且伴随着一个缺点,即编写无效代码非常容易 - 而且多重继承也是如此。因此,存在引导程序员远离此功能的趋势。

其他人已经解决了多重继承不利的问题。但我们已经看到了不少评论,或多或少意味着避免它的原因是因为它不安全。嗯,是的,不。

正如在C ++中经常这样,如果您遵循基本准则,您可以安全地使用它,而不必经常“俯视”。关键的想法是你区分一种称为“混合”的特殊类定义;如果所有成员函数都是虚拟(或纯虚拟),则class是混合。然后你可以从一个主类和你喜欢的“混合”继承 - 但是你应该使用关键字“virtual”继承mixins。例如

class CounterMixin {
    int count;
public:
    CounterMixin() : count( 0 ) {}
    virtual ~CounterMixin() {}
    virtual void increment() { count += 1; }
    virtual int getCount() { return count; }
};

class Foo : public Bar, virtual public CounterMixin { ..... };

我的建议是,如果你打算使用一个类作为混合类,你也可以采用命名约定,让任何阅读代码的人都可以轻松查看正在发生的事情,并根据基本准则的规则验证你是否正在玩。如果你的混合版本也有默认的构造函数,那么你会发现它的效果要好得多,这只是因为虚拟基类的工作方式。并且记得让所有的析构函数都是虚拟的。

请注意,我在这里使用“混合”一词与参数化模板类不同(请参阅this link以获得一个很好的解释),但我认为这是对术语的合理使用。

现在我不想给人的印象是这是安全使用多重继承的唯一方法。这只是一种相当容易检查的方式。


2
投票

冒着抽象的风险,我发现在类别理论框架内考虑继承是很有启发性的。

如果我们想到它们之间的所有类和箭头表示继承关系,那么就像这样

A --> B

意味着class B来自class A。请注意,给定

A --> B, B --> C

我们说C来源于B,它来自A,所以C也被认为是从A推导出来的

A --> C

此外,我们说,对于每个类A来说,A来自A,因此我们的继承模型实现了类别的定义。在更传统的语言中,我们有一个类别Class与对象所有类和态射的继承关系。

这是一个设置,但有了这个让我们来看看我们的毁灭之钻:

C --> D
^     ^
|     |
A --> B

这是一个阴暗的图表,但它会做。所以D继承了所有ABC。此外,并且越来越接近解决OP的问题,D也继承了A的任何超类。我们可以绘制一个图表

C --> D --> R
^     ^
|     |
A --> B
^ 
|
Q

现在,与钻石死亡相关的问题是当CB分享一些属性/方法名称时,事情变得模棱两可;但是,如果我们将任何共享行为移动到A,那么模糊性就会消失。

换句话说,我们希望ABC是这样的,如果BC继承自Q,那么A可以被重写为Q的子类。这使A称为pushout

D上还有一个称为pullback的对称结构。这基本上是你可以构造的最通用的有用类,它继承自BC。也就是说,如果你有任何其他类R乘以BC,那么D是一个类,其中R可以被重写为D的子类。

确保您的钻石提示是拉回和推出为我们提供了一个很好的方法来一般地处理可能出现的名称冲突或维护问题。

注意Paercebalanswer启发了这一点,因为我们在所有可能类的完整类别Class中工作,因为上述模型暗示了他的告诫。

我想把他的论点推广到一些东西,它表明复杂的多重继承关系既强大又无问题。

TL; DR将程序中的继承关系视为形成类别。然后你可以通过多次继承的类推出和对称来避免Diamond of Doom问题,从而形成一个共同的父类,这是一个回调。


2
投票

我们用艾菲尔铁塔我们有优秀的MI。别担心。没有问题。易于管理。有时候不使用MI。然而,它比人们意识到的更有用,因为它们是:A)处于危险的语言中,不能很好地管理它 - 或者B)对他们如何在MI中工作多年和多年感到满意 - 或 - C)其他原因(太多了不能列出我很确定 - 见上面的答案)。

对我们来说,使用Eiffel,MI就像其他任何东西一样自然,也是工具箱中的另一个好工具。坦率地说,我们完全不关心没有人使用艾菲尔。别担心。我们对我们的产品感到满意,并邀请您一起来看看。

当你正在寻找:特别注意空洞安全和消除空指针解除引用。虽然我们都在MI周围跳舞,但你的指针却迷失了! :-)

© www.soinside.com 2019 - 2024. All rights reserved.