为什么我应该避免在C ++中的多重inheritance?

使用多重inheritance是一个好概念,还是我可以做其他事情?

多重inheritance(缩写为MI)的气味 ,这意味着通常这样做是因为不好的原因,并且会在维护者面前反击。

概要

  1. 考虑function的组成,而不是inheritance
  2. 警惕恐怖钻石
  3. 考虑多个接口而不是对象的inheritance
  4. 有时候,多重inheritance是正确的。 如果是,那就用它。
  5. 准备在代码评论中捍卫您的多inheritance体系结构

1.也许组成?

这对于inheritance是正确的,所以对于多inheritance来说更是如此。

你的对象是否真的需要从另一个inheritance? Car不需要从一台Engineinheritance工作,也不需要从一个Wheelinheritance。 一辆Car有一个Engine和四个Wheel

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

2.恐惧钻石

通常情况下,你有一个类A ,然后BC都从Ainheritance。 (不要问我为什么)然后有人决定D必须inheritanceBC

八八年我曾两次遇到过这样的问题,因为:

  1. 从一开始就有多less错误(在这两种情况下, D不应该从BC都inheritance),因为这是糟糕的架构(事实上, C根本不应该存在…)
  2. 有多less维护人员为此付出了代价,因为在C ++中,父类A在其子孙D类中出现了两次,因此更新一个父字段A::field意味着更新它两次(通过B::fieldC::field ),或者让某些事情默默无闻地崩溃,之后(在B::field新增一个指针,并删除C::field …)

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

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

更多关于钻石(编辑2017-05-03)

在C ++中恐惧钻石的真正问题( 假设devise是合理的 – 请检查你的代码! ), 你需要做出一个select

  • A类在布局中存在两次是可取的,这是什么意思? 如果是的话,那么一定要从它那里inheritance两次。
  • 如果它只存在一次,那么就从它那里虚拟地inheritance下来。

这个select是固有的问题,在C ++中,不像其他语言,你可以做到这一点,没有教条强迫你的devise在语言层面。

但是像所有的权力一样,这个权力是责任:你的devise是否被审查过。

3.接口

多个零或一个具体类的inheritance,零个或多个接口通常是好的,因为你不会遇到上面描述的恐惧钻石。 事实上,这是怎么在Java中完成的。

通常,当C从ABinheritance时,意思是用户可以使用C ,就好像它是A ,并且/或者就好像它是B

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

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

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

更多关于C ++抽象接口(编辑2017-05-03)

首先,NVI模式可以用来产生一个接口,因为真正的标准是没有状态 (即没有成员variables,除此之外)。 你的抽象接口的重点是发布一个合同(“你可以这样称呼我”),除此之外别无其他。 只有抽象虚拟方法的局限性应该是deviseselect,而不是义务。

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

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

4.你真的需要多重inheritance吗?

有时候是。

通常情况下,你的C类是inheritance自AB ,而AB是两个不相关的对象(即不在同一个层次结构中,没有共同之处,不同的概念等)。

例如,你可以有X,Y,Z坐标系的Nodes ,可以做很多的几何计算(也许是一个点,几何对象的一部分),每个节点是一个自动代理,能够与其他代理。

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

所以你有你自己own::Nodeai::Agentgeo::Point派生。

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

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

(你会注意到ai::Agentgeo::Point是完全的,完全不相关的…这大大降低了多重inheritance的风险)

其他情况(编辑2017-05-03)

还有其他的情况:

  • 使用(希望私有)inheritance作为实现细节
  • 一些C ++习惯用法(如策略)可以使用多重inheritance(当每个部分需要通过this与其他部分进行通信时)
  • 来自std :: exception的虚inheritance( 虚拟inheritance是否是例外所必需的? )
  • 等等

有时你可以使用组合,有时候MI更好。 重点是:你有一个select。 负责任地做(并且检查你的代码)。

那么,我应该做多重inheritance吗?

大部分时间,根据我的经验,没有。 心肌梗死并不是正确的工具,即使它似乎工作,因为它可以被懒惰用来堆积特征而没有意识到后果(如制作一个Engine和一个Wheel )。

但有时候,是的。 那时候,没有什么比MI更好的了。

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

在接受Bjarne Stroustrup采访时,

人们相当正确地说,你不需要多inheritance,因为你可以用多inheritance来做任何事情,你也可以用单inheritance来做。 你只是使用我提到的代表团把戏。 而且,根本不需要任何inheritance,因为任何你使用单一inheritance的事情,你也可以通过类inheritance而不需要inheritance。 实际上,你也不需要任何类,因为你可以用指针和数据结构来完成。 但是,你为什么要这样做呢? 什么时候使用语言设施方便? 你什么时候更喜欢解决方法? 我见过多重inheritance是有用的情况,我甚至看到了很复杂的多重inheritance是有用的情况。 一般来说,我更喜欢使用该语言提供的工具来解决问题

没有理由避免它,它可以在情况下非常有用。 你需要意识到潜在的问题。

最大的一个是死亡的钻石:

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

你现在有两个“副本”的爷爷在孩子之内。

C ++已经想到了这一点,并让你做虚拟inheritance来解决这个问题。

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

总是检查你的devise,确保你没有使用inheritance来节省数据重用。 如果你可以用组合来expression同样的东西(通常你可以),这是一个更好的方法。

见w: 多重inheritance 。

多重inheritance受到了批评,因此没有在许多语言中实现。 批评包括:

  • 复杂性增加
  • 语义模糊常常被概括为钻石问题 。
  • 不能从一个类中显式inheritance多次
  • inheritance顺序改变类的语义。

C ++ / Java风格构造函数的多语言inheritance加剧了构造函数和构造函数链的inheritance问题,从而在这些语言中产生维护和扩展性问题。 在构造函数链式范式下很难实现具有不同构造方法的inheritance关系中的对象。

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

我可以做其他的事情来代替这个吗?

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

  • 编程到一个接口,而不是一个实现
  • 比inheritance更喜欢构图

公有inheritance是一种IS-A关系,有时候一个类将会是几个不同类的一种types,有时反映这一点很重要。

“Mixins”有时也是有用的。 它们通常是小类,通常不会从任何东西inheritance,提供有用的function。

只要inheritance层次是相当浅的(因为它应该几乎总是),而且pipe理得当,你不可能得到可怕的钻石inheritance。 钻石并不是所有使用多重inheritance的语言的问题,但C ++对它的处理往往是尴尬的,有时令人费解。

虽然我碰到多重inheritance非常方便的情况,但实际上它们相当罕见。 这可能是因为我更喜欢使用其他devise方法,当我不需要多重inheritance。 我更喜欢避免混淆的语言结构,并且很容易构造inheritance的情况下,你必须非常好地阅读手册来弄清楚发生了什么事情。

你不应该“避免”多重inheritance,但是你应该知道可能出现的问题,比如“钻石问题”( http://en.wikipedia.org/wiki/Diamond_problem ),谨慎对待给予你的权力,你应该拥有所有的权力。

你应该小心使用它,有些情况下,比如钻石问题 ,当事情变得复杂的时候。

替代文字http://www.learncpp.comhttp://img.dovov.comCppTutorial/Section11/PoweredDevice.gif

每种编程语言都有不同的处理面向对象编程的优点和缺点。 C ++的版本把重点放在了性能上,并且有一个不利的地方,那就是编写无效的代码非常容易,这是多重inheritance。 因此,有一种趋势是让程序员远离这个特性。

其他人已经解决了多重inheritance不适合的问题。 但是我们看到了不less意见,这或多或less地暗示了避免这个问题的原因是因为它不安全。 那么,是的,不。

在C ++中通常是这样的,如果你遵循一个基本的指导原则,你可以安全地使用它,而不必一直“眺望你的肩膀”。 关键的想法是,你区分一种特殊的类定义,称为“混合”。 如果所有的成员函数都是虚拟的(或者纯虚拟的),那么class就是一个混合。 然后,您可以从一个主类inheritance,并且尽可能多的“混合”,但是您应该inheritance关键字“virtual”的mixin。 例如

 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 { ..... }; 

我的build议是,如果您打算将课程作为混合课程,那么您也可以使用命名约定,以便读者能够轻松看到发生的事情,并根据基本指南的规则来validation您是否玩游戏。 而且你会发现,如果你的混合插件也有默认的构造函数,就会因为虚拟基类的工作方式而变得更好。 记住所有的析构函数也是虚拟的。

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

现在我不想给人这样的印象,这是安全使用多重inheritance的唯一方法。 这只是一个很容易检查的方法。

有点抽象的风险,我觉得在类别理论的框架内思考inheritance的照明。

如果我们把他们之间的所有阶级和箭头都看成是inheritance关系,那么就是这样的

 A --> B 

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

 A --> B, B --> C 

我们说C是从B派生而来的,所以C也被认为是从A派生的,因此

 A --> C 

此外,我们说,对于每A平凡AA派生的A ,我们的inheritance模型满足了一个范畴的定义。 在更传统的语言中,我们有一个ClassClass的对象所有类和态射的inheritance关系。

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

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

这是一个阴暗的图表,但它会做的。 所以DABC所有inheritance。 而且,越来越接近OP的问题, D也inheritance了A任何超类。 我们可以绘制一个图表

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

现在,与死亡钻石相关的问题是,当CB共享一些财产/方法名称,而事物变得模糊时, 但是,如果我们将任何共享的行为移动到A那么歧义消失。

在分类术语中,我们希望ABC是这样的,如果BCQinheritance,则A可以被重写为Q子类。 这使得A被称为推出的东西。

D上也有一个对称的结构叫做回撤 。 这实质上是你可以构造的最通用的有用的类,它从BC都inheritance。 也就是说,如果你有任何从BCinheritance的其他R类乘法,那么D是一个类,其中R可以被重写为D子类。

确保你的钻石的提示是callback和推出给我们一个很好的方式来一般处理名称冲突或维护问题,否则可能出现。

注意 Paercebal的答案启发了这一点,因为他的训诫是由上述模型暗示的,因为我们工作在所有可能的类的完整类别类。

我想把他的论点推广到一些东西上来,这些东西说明了复杂的多重inheritance关系是多么强大和没有问题。

TL; DR将程序中的inheritance关系看作是一个类别。 然后你可以通过多次inheritance的类推出和对称的方式来避免死亡钻石的问题,使一个共同的父类是一个callback。

inheritance的使用和滥用。

这篇文章在解释inheritance方面做得很好,而且是危险的。

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

构图本质上容易理解,理解和解释。 编写代码非常繁琐,但是一个好的IDE(我已经使用了Visual Studio已经有几年了,但是当然Java IDE都有很好的组合快捷方式自动化工具)可以帮助你渡过难关。

另外,就维护而言,“菱形问题”也出现在非文字inheritance的情况下。 例如,如果你有A和B,你的C类扩展了它们,而A有一个“makeJuice”方法,那么你就可以用橙汁来制作橙汁了:当devise师为“ B'增加一个'makeJuice'的方法来产生电stream? 'A'和'B' 现在可以兼容“父母”,但这并不意味着他们永远都是如此!

总体而言,避免inheritance的准则,特别是多重inheritance是正确的。 正如所有格言一样,也有例外,但是您需要确保有一个闪烁的绿色霓虹灯,指示您编码的任何exception情况(并训练您的大脑,以便您在任何时候看到您在自己闪烁的绿色霓虹灯中绘制的inheritance树签署),并检查确保每隔一段时间都有意义。

MI对于具体对象的关键问题在于,你很less有一个合法的“成为A而成为B”的对象,所以在逻辑上很less是正确的解决scheme。 更经常地,你有一个对象C服从“C可以作为A或B”,你可以通过接口inheritance和组合实现。 但是请不要误解,多个接口的inheritance仍然是MI,只是它的一个子集。

对于C ++来说,这个特性的关键弱点并不是多重inheritance的实际存在,但是它允许几乎总是格式错误的构造。 例如,inheritance同一对象的多个副本,如下所示:

 class B : public A, public A {}; 

由定义格式不正确。 翻译成英文是“B是A和A”。 所以,即使在人类语言中也存在着严重的歧义。 你的意思是“B有2个”或者“B是个A”? 允许这样的病态代码,更糟糕的是使它成为一个用法的例子,C ++没有优先考虑保持后继语言的特征。

您可以使用组合优先于inheritance。

总的感觉是组合更好,讨论得非常好。

每个类涉及4/8个字节。 (每个类一个这个指针)。

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

我们用艾菲尔。 我们拥有出色的MI。 别担心。 没有问题。 轻松pipe理。 有时候不要使用MI。 然而,它比人们意识到的有用,因为它们是:A)用一种危险的语言来pipe理它,不然B)对他们在MI工作多年和几年的方式感到满意 – 或者C)其他原因不胜枚举我很确定 – 见上面的答案)。

对于我们来说,使用艾菲尔,MI就像工具箱里的其他东西一样自然。 坦率地说,我们完全不关心别人在用艾菲尔。 别担心。 我们很高兴与我们有什么,并邀请您来看看。

在你看的时候:特别注意虚空安全和消除空指针解引用。 当我们都在MI附近跳舞的时候,你的指针会迷路了! 🙂