为什么要避免在C ++中进行多重继承?


Answers:


259

多重继承(缩写为MI)闻起来,这意味着通常是由于不好的原因完成的,并且会在维护者面前大发雷霆。

摘要

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

1.也许是构图?

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

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

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

2.恐惧之钻

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

我在八八年中两次遇到这种问题,由于以下原因,我很高兴看到:

  1. 从一开始就犯了多少错误(在两种情况下D都不应继承自Band C),因为这是糟糕的体系结构(实际上C根本不应该存在……)
  2. 维护者为此付出了多少钱,因为在C ++中,父类A在其子孙类中出现了两次D,因此,更新一个父字段A::field意味着要么对其进行两次更新(通过B::fieldC::field),要么在以后出现一些无提示的错误并崩溃。 (在中新建一个指针B::field,然后删除C::field...)

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

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

有关钻石的更多信息(编辑2017-05-03)

C ++中的Diamond of Dread的真正问题(假设设计是正确的-审查您的代码!),您需要做出选择

  • 希望类A在您的布局中存在两次,这是什么意思?如果是,那么一定要继承两次。
  • 如果它应该只存在一次,则从其虚拟继承。

这种选择是问题的固有选择,并且在C ++中,与其他语言不同,您实际上可以做到这一点而无需在语言级别强迫设计。

但是,像所有权力一样,权力也伴随着责任:审查设计。

3.接口

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

通常情况下,你是什么意思时由C继承AB是用户可以使用C,如果它是一个A,和/或如果它是一个B

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

  1. 其所有方法都声明为纯虚拟方法(后缀= 0) (已从2017-05-03移除)
  2. 没有成员变量

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

有关C ++抽象接口的更多信息(edit 2017-05-03)

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

其次,在C ++中,实际上可以从抽象接口继承(即使有额外的开销/间接)也是有意义的。如果您不这样做,并且接口继承在层次结构中多次出现,那么您将有歧义。

第三,面向对象很棒,但它不是唯一的真相TM在C ++中。使用正确的工具,并且永远记住您在C ++中还有其他范式可以提供不同类型的解决方案。

4.您真的需要多重继承吗?

有时候是的。

通常情况下,你的C类继承AB,和AB是两个不相关的对象(即不在同一层次,没有任何共同之处,不同的概念,等等)。

例如,您可能有一个 Nodes具有X,Y,Z坐标的系统,能够执行许多几何计算(也许是一个点,一部分几何对象),并且每个节点都是一个自动代理,可以与其他代理进行通信。

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

因此,您有自己的own::Node来自ai::Agent和的派生geo::Point

在这一刻,您应该问自己是否不应该使用合成。如果own::Node确实确实是a ai::Agent和a geo::Point,那么合成将不起作用。

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

(您会注意到,ai::Agent并且geo::Point它们是完全,完全,完全不相关的……这大大降低了多重继承的危险)

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

还有其他情况:

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

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

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

根据我的经验,大多数时候没有。即使它看起来可行,MI也不是正确的工具,因为懒惰者可以使用它来将要素堆积在一起,而不会意识到后果(例如使Caran Engine和a 都产生Wheel)。

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

但是因为MI很臭,所以要准备在代码审查中捍卫您的体系结构(捍卫它是一件好事,因为如果您不能捍卫它,那么您就不应该这样做)。


4
我认为这是没有必要的,而且很少有用,以至于即使很合适,通过委派节省的费用也无法弥补不习惯使用它的程序员的困惑,反之则是很好的总结。
Bill K

2
我要补充一点,第5点是,使用MI的代码应该在其旁边带有注释以说明原因,因此您不必在代码审查中进行口头解释。如果没有这个,那么不是您的审阅者的人在看到您的代码时可能会质疑您的判断力,而您可能没有机会捍卫它。
蒂姆·艾贝尔

因为您不会遇到上述的恐惧钻石。 ”为什么不呢?
curiousguy 2011年

1
我将MI用于观察者模式。
Calmarius 2012年

13
@BillK:当然没有必要。如果您拥有不带MI的Turning完整语言,则可以使用MI进行任何语言处理。所以是的,没有必要。曾经 话虽如此,它可能是一个非常有用的工具,所谓的“恐惧钻石”……我从来没有真正理解过为什么“恐惧”这个词甚至还存在。实际上,推理起来很容易,而且通常不是问题。
Thomas Eding

145

从一个 Bjarne Stroustrup采访

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


6
我已经在C#和Java中错过了C ++的一些功能,尽管拥有它们会使设计更加困难/复杂。例如,不变性的保证const- 当类确实需要具有可变和不变的变量时,我不得不编写笨拙的变通方法(通常使用接口和组合)。但是,我从来没有一次错过了多重继承,也从来没觉得我写了一个解决方法由于缺乏此功能。那是区别。在我见过的每种情况下,使用MI是更好的设计选择,而不是解决方法。
BlueRaja-Danny Pflughoeft 2011年

24
+1:完美答案:如果您不想使用它,那就不要使用它。
Thomas Eding

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;

始终检查设计,确保不使用继承来节省数据重用。如果您可以用合成来表示同一件事(通常可以),那么这是一种更好的方法。


21
如果你禁止继承,只有使用的组合物替代,你总是有两个GrandParentChild。人们担心MI,因为人们只是认为他们可能不理解语言规则。但是,任何无法获得这些简单规则的人,也无法编写非平凡的程序。
curiousguy

1
听起来很刺耳,所有C ++规则都很详尽。好的,所以即使单个规则很简单,也有很多规则,这使得整个过程变得不那么简单,至少对于人类来说是要遵循的规则。
斑马鱼

11

请参阅w:多重继承

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

  • 复杂性增加
  • 语义歧义通常被概括为钻石问题
  • 无法从一个类显式继承多次
  • 继承顺序更改类语义。

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

解决此问题的现代方法是使用接口(纯抽象类),例如COM和Java接口。

我可以代替此做其他事情吗?

是的你可以。我将从GoF窃取。

  • 编程到接口而不是实现
  • 优先考虑组成而不是继承

8

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

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

只要继承层次结构很浅(几乎应该总是如此)并且管理得当,您就不太可能获得可怕的钻石继承。钻石并不是所有使用多重继承的语言都存在的问题,但是C ++对它的处理通常很尴尬,有时令人困惑。

尽管我遇到了多重继承非常方便的情况,但实际上它们很少见。这可能是因为我确实不需要多重继承时更喜欢使用其他设计方法。我确实希望避免混淆语言构造,并且构造继承案例很容易,在这种情况下,您必须非常好地阅读手册以弄清楚发生了什么。


6

您不应该“避免”多重继承,但是您应该意识到可能出现的问题,例如“钻石问题”(http://en.wikipedia.org/wiki/Diamond_problem),并谨慎对待赋予您的权力,如您所愿。


1
+1:就像您不应该避免使用指针一样;您只需要了解其后果即可。人们需要停止在网络上学到一些短语(例如“ MI是邪恶的”,“不要优化”,“ goto是邪恶的”),因为一些嬉皮士说这对他们的卫生有害。最糟糕的是,他们甚至从未尝试过使用这种东西,而只是像圣经所说的那样。
Thomas Eding

1
我同意。不要“避免”它,但是知道处理问题的最佳方法。也许我宁愿使用合成特征,但是C ++没有。也许我宁愿使用“句柄”自动委派,但是C ++没有。因此,我可能同时使用MI的常规工具和钝器来实现多种不同目的。
JDługosz

3

冒着有点抽象的风险,我发现在范畴论的框架内考虑继承很有启发性。

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

A --> B

表示class B从衍生class A。注意,给定

A --> B, B --> C

我们说C衍生自B,而B衍生自A,因此C也被称为衍生自A,因此

A --> C

此外,我们说对于每个从A琐碎A派生的类A,因此我们的继承模型满足类别的定义。在更传统的语言中,我们有一个类别,Class其中包含对象,所有类别和词素的继承关系。

这有点设置,但是让我们看一下《毁灭钻石》:

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

这是一张阴暗的图表,但可以。所以D从所有ABC。此外,D随着越来越接近解决OP的问题,它也继承自的任何超类A。我们可以画一个图

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

现在,死亡的钻石相关的问题在这里是当CB大家分享一些属性/方法的名字和事情变得不明确; 但是,如果我们将任何共享行为转移到A那么歧义就会消失。

用分类的术语来说,我们想要AB并且C要使得if B和从那时C继承QA可以被重写为的子类Q。这使得A一种叫做pushout

还有一种D称为回撤的对称构造。从本质上讲,这是您可以构造的最通用的有用类,该类均从B和继承C。也就是说,如果您有其他任何类都RB和的类C,则D该类是R可以重写为的子类的类。D

确保您的钻石技巧是回调和压出,这为我们提供了一种很好的方式来处理一般情况下的名称冲突或维护问题。

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

我想将他的论点概括为某种东西,以显示复杂的多重继承关系既强大又无问题。

TL; DR可以将程序中的继承关系视为一个类别。然后,可以通过以多重继承的方式推出并对称地制作通用的父类(即回撤)来避免“毁灭之钻石”问题。


3

我们使用埃菲尔铁塔。我们有出色的MI。别担心。没有问题。易于管理。有时不使用MI。但是,它比人们意识到的有用得多,因为他们是:A)用危险语言无法很好地处理-OR- B)对他们多年来围绕MI的工作方式感到满意-OR- C)其他原因(太多我无法确定的清单-请参阅上面的答案)。

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

在查找时:请特别注意Void安全性和Null指针取消引用的消除。当我们都围绕着MI跳舞时,您的指针迷路了!:-)


2

每种编程语言对面向对象编程的优缺点都略有不同。C ++的版本将重点完全放在性能上,并具有附带的缺点,即编写无效代码非常容易使人烦恼-多继承确实如此。结果,存在使程序员远离该功能的趋势。

其他人已经解决了多重继承不适合什么的问题。但是,我们已经看到很多评论,或多或少暗示着避免它的原因是因为它不安全。好吧,是的,不是。

就像在C ++中经常发生的那样,如果您遵循基本准则,则可以安全地使用它,而不必经常“抬头”。关键思想是您区分一种特殊的类定义,称为“混合”。如果类的所有成员函数都是虚拟的(或纯虚拟的),则为类的混合。然后,您可以从单个主类继承并且可以随意继承多个“ mix-ins”,但是您应该继承关键字为“ 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 { ..... };

我的建议是,如果您打算将某个类用作混合类,则还应采用一种命名约定,以使任何阅读代码的人都可以轻松地查看正在发生的事情,并根据基本准则来验证您是否在玩。而且,您会发现,如果混入也具有默认构造函数,则效果会更好,这仅仅是因为虚拟基类的工作方式。并记住也将所有析构函数也设为虚拟。

请注意,我在这里使用的“混入”一词与参数化的模板类不同(请参阅此链接以获取详细说明),但我认为这是对术语的合理使用。

现在,我不想给人留下这样的印象,那就是安全地使用多重继承的唯一方法。这只是一种很容易检查的方法。


2

您应该谨慎使用它,在某些情况下,例如“ 钻石问题”,事情可能会变得复杂。

替代文字
(来源:learncpp.com


12
这是一个不好的例子,因为复印机应该由扫描仪和打印机组成,而不是从它们那里继承。
Svante

2
无论如何,a Printer甚至都不应该是a PoweredDevice。A Printer是用于打印,而不是电源管理。特定打印机的实现可能必须执行一些电源管理,但是这些电源管理命令不应直接暴露给打印机用户。我无法想象现实世界会使用这种层次结构。
curiousguy


1

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

组合本质上易于理解,理解和解释。编写代码可能很繁琐,但是一个好的IDE(自从我使用Visual Studio以来已经有好几年了,但是Java IDE都具有出色的合成快捷方式自动化工具)应该可以帮助您克服这一难题。

同样,在维护方面,非文字继承实例中也会出现“钻石问题”。例如,如果您有A和B,并且您的类C都将它们都扩展了,而A有一个“ makeJuice”方法可以制作橙汁,那么您可以将其扩展为用石灰混合来制作橙汁:当“ B'添加了一个“ makeJuice”方法,该方法会产生电流?“A”和“B”可能是兼容的“父母” ,现在,但是,这并不意味着他们将永远是这样的!

总的来说,避免继承,特别是避免多重继承的原则是合理的。作为所有准则,都有例外,但是您需要确保有一个闪烁的绿色霓虹灯指向您编码的任何例外(并训练您的大脑,以便您每次看到此类继承树时,都可以在自己的闪烁的绿色霓虹灯中绘制签名),然后您检查一下以确保所有内容每隔一段时间都有意义。


what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current?嗯,您当然会遇到编译错误(如果使用不明确)。
Thomas Eding

1

具体对象的MI的关键问题在于,很少有对象合法地应“成为A而成为B”,因此,从逻辑上讲,这很少是正确的解决方案。通常,您有一个对象C遵循“ C可以充当A或B”,您可以通过接口继承和组合来实现。但是,请不要出错,多个接口的继承仍然是MI,只是它的一个子集。

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

class B : public A, public A {};

根据定义格式不正确。翻译成英文是“ B是A和A”。因此,即使在人类语言中,也存在严重的歧义。您是说“ B有2个As”还是“ B是A”?允许这样的病态代码,更糟糕的是使它成为一个使用示例,当涉及到将功能保留在后续语言中的理由时,C ++并没有受到青睐。



0

每个类占用4/8个字节。(每个类一个此指针)。

可能永远不会担心,但是如果有一天您拥有一个被实例化了数十亿次的微数据结构。


1
可以吗 通常,任何严重的继承都需要在每个类成员中都有一个指针,但是这是否必然随继承的类扩展?更重要的是,您拥有数十亿个小数据结构?
David Thornley,2009年

3
@DavidThornley“ 您有数十亿个小数据结构? ”当您提出反对MI的论点时。
curiousguy 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.