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

18 浏览
0 Comments

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

使用多重继承是一个好的概念吗?还是我可以选择其他替代方案?

0
0 Comments

为什么应该避免在C++中使用多重继承?

在C++中,多重继承是一种强大的功能,可以在某些情况下非常有用。然而,我们需要注意其中的潜在问题。

最大的问题是"死亡之钻"(diamond of death)的出现。在下面的代码中,我们可以看到这个问题:

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;

在设计代码时,我们应该始终审查我们的设计,并确保我们不是为了节省数据重用而使用继承。如果我们可以使用组合来表示相同的概念(通常是可以的),那么这是一个更好的方法。

此外,如果我们禁止继承,只使用组合,那么在Child类中我们仍然会有两个"GrandParent"的实例。对于多重继承的担忧,是因为人们认为自己可能不理解语言规则。但是,任何不能理解这些简单规则的人也不能编写一个非平凡的程序。

听起来有些严厉,但是C++的规则非常复杂。即使每个规则本身很简单,但总共有很多规则,这使得整个过程对于一个人来说并不简单。因此,我们需要谨慎使用多重继承,并确保我们理解并遵循语言的规则。

0
0 Comments

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

在C++中,多重继承是一种可以同时从多个基类继承属性和方法的机制。然而,多重继承也带来了一些问题和复杂性,因此有人建议尽量避免使用多重继承。

原因:

1. 可以通过单一继承和委托技巧来实现多重继承的功能,因此有人认为多重继承并不是必需的。使用委托可以将多个基类的功能委托给一个单一继承的类。

2. 单一继承和委托技巧可以完全替代继承的功能,因此可以不使用继承来实现相同的功能。通过透过一个类进行转发,可以实现不用继承的单一继承功能。

3. 如果不使用继承,甚至可以使用指针和数据结构来实现相同的功能。然而,这种做法通常会增加代码复杂性,降低代码的可读性和可维护性。

解决方法:

1. 避免使用多重继承,而是使用单一继承和委托技巧来实现相同的功能。这样可以减少代码的复杂性,并使代码更易于理解和维护。

2. 如果确实需要使用多重继承,确保仔细考虑设计和继承关系,以避免出现复杂的多重继承结构。

尽管C++中提供了多重继承的功能,但在实际编程中,避免使用多重继承通常是更好的设计选择。通过使用单一继承和委托技巧,可以实现相同的功能,同时减少代码复杂性和提高代码可读性。尽管在某些情况下多重继承可能有用,但在大多数情况下,避免使用多重继承可以避免出现设计上的复杂性和问题。因此,如果不需要使用多重继承,最好不要使用它。

0
0 Comments

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

多重继承(MI)通常是出于不良原因而进行的,这将对维护者造成困扰。

1. 考虑使用组合而不是继承

2. 警惕“恐怖的钻石”

3. 考虑多重接口继承而不是对象继承

4. 有时,多重继承是正确的选择。如果是这样,请使用它。

5. 准备在代码审查中为你的多重继承架构进行辩护

1. 或许使用组合?

这对于继承是正确的,所以对于多重继承来说更为正确。

你的对象真的需要继承另一个对象吗?一个汽车并不需要继承引擎或者轮子。一个汽车有一个引擎和四个轮子。

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

2. 恐怖的钻石

通常,你有一个类A,然后B和C都继承自A。然后(不要问我为什么),有人决定D必须同时继承B和C。

我在八年中遇到了两次这样的问题,很有趣的是:

- 从一开始就犯了多大的错误(在这两种情况下,D不应该同时继承B和C),因为这是不好的架构(实际上,C根本不应该存在...)

- 维护者为此付出了多大的代价,因为在C++中,父类A在其孙类D中出现了两次,所以更新一个父类字段A::field意味着要么更新两次(通过B::field和C::field),要么出现一些隐性错误并崩溃(在B::field中新建一个指针,然后删除C::field...)

在C++中,使用关键字virtual来修饰继承可以避免上述描述的重复布局,但无论如何,根据我的经验,你可能正在做一些错误的事情。

在对象层次结构中,你应该尽量将层次结构保持为一棵树(一个节点只有一个父节点),而不是一个图。

3. 接口

通常,多重继承零个或一个具体类和零个或多个接口是可以的,因为你不会遇到上述描述的“恐怖的钻石”问题。事实上,这就是Java中的做法。

通常,当C继承自A和B时,你的意思是用户可以将C当作A使用,和/或当作B使用。

在C++中,接口是一个具有以下特点的抽象类:

- 所有方法都声明为纯虚函数(后面加上= 0)

- 没有成员变量

多重继承零到一个真实对象和零个或多个接口并不被认为是“有问题的”(至少不会有那么多问题)。

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

有时候是的。

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

例如,你可以有一个带有X、Y、Z坐标的“节点”系统,能够进行许多几何计算(也许是一个点,作为几何对象的一部分),而且每个节点都是一个能够与其他代理进行通信的自动化代理。

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

所以你的“own::Node”从“ai::Agent”和“geo::Point”同时继承。

这是你应该问自己是否应该使用组合而不是多重继承的时候。如果“own::Node”真的既是“ai::Agent”又是“geo::Point”,那么组合是不适用的。

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

(你会注意到,“ai::Agent”和“geo::Point”是完全无关的...这大大减少了多重继承的危险性)

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

在我看来,大多数情况下都不应该使用多重继承。即使它似乎能够正常工作,因为它可能被懒惰的程序员用来堆叠功能,而没有意识到后果(比如让“Car”既是“Engine”又是“Wheel”)。

但有时候,是的。这时候,没有什么比多重继承更好的了。

但因为多重继承有问题,所以准备在代码审查中为你的架构进行辩护(并进行辩护是一件好事,因为如果你无法为之辩护,那么你就不应该这样做)。

我认为它从来都不是必需的,所以即使它是一个完美的适应,与委托相比的节省也无法弥补对不习惯使用它的程序员的困惑,但总的来说是个很好的总结。

我会在第5点中添加一个评论,说明使用多重继承的原因,这样你在代码审查中就不需要口头解释了。如果没有这个评论,那么当其他人看到你的代码时,可能会质疑你的判断力,而你可能没有机会进行辩护。

“因为你不会遇到上述描述的“恐怖的钻石”问题。”为什么不会?

因为如果你从0或1个具体类和0到N个接口中进行多重继承,代码和成员变量的实现来自当前类或基类具体类。唯一的编译时模糊之处是,如果你的类C继承自基类具体类B,它有一个foo方法,而C也继承自接口I,该接口也声明了一个纯虚方法foo。你的类C将不得不声明自己的C::foo,调用B::foo。

我经常在观察者模式中使用多重继承。

有趣的是,你在第4点中提到了ai::Agent和geo::Point。我正在研究是否应该使用多重继承或组合,这基本上就是我要解决的问题:一个在游戏中既是物理实体又具有行为(是FSM的一部分)的“代理”。我会选择多重继承,因为对我来说更自然,并且你的帖子证实了这一点!

当然,它并不是必需的。如果你有一个没有多重继承的完整语言,你可以做任何一个有多重继承的语言可以做到的事情。所以是的,它不是必需的。永远不是。也许,它可以成为一个非常有用的工具,所谓的“恐怖的钻石”...我从来没有真正明白为什么会有“恐怖”这个词。实际上,它很容易理解,并且通常不是一个问题。我认为你可能有很多解决方案,可以使用多重继承也可以不使用。其中95%的解决方案如果不使用多重继承会更好,另外5%则是一个抉择,但在这两种情况下,多重继承版本可能更难理解和维护。当然,如果你是为了好玩而编写代码,那就尽情享受吧!但编写公司的代码的目的不是一个工作正常的产品,而是作为产品的组成部分,下一个人可以轻松地接手和维护。

我对第4点中的示例不是很满意,因为这两个类是如此不相关,用组合可能是一个解决方案(使用转发方法,使用助手方法获取内部对象进行接口目的)。我猜如果两者需要通信(通过“this”)或者如果方法需要被重写,那么这个示例会更好一些。

“通常情况下,当C从A和B继承时,你的意思是用户可以将C当作A使用,和/或当作C使用。”我认为你是指“...当作A使用,和/或当作B使用。”在这里指出,以防其他人阅读时混淆。

我经常按照第3点描述的方式使用多重继承,通常不会出现任何问题。我通常通过从一组较小的接口中继承来构建一个新的接口,然后在具体对象中实现这个单一的组合接口。

0