为什么我应该避免 C++ 中的多重继承?
使用多重继承是个好概念还是我可以做其他事情?
解决方案多继承(缩写为 MI)smells,意思是通常,它是做不好的原因,会在维护者面前反击.
总结
- 考虑组合特征,而不是继承
- 小心恐惧之钻
- 考虑继承多个接口而不是对象
- 有时,多重继承是正确的.如果是,请使用它.
- 准备好在代码审查中捍卫您的多重继承架构
1.也许是组合?
这适用于继承,因此,更适用于多重继承.
你的对象真的需要从另一个继承吗?Car
不需要从 Engine
继承来工作,也不需要从 Wheel
继承.一个 Car
有一个 Engine
和四个 Wheel
.
如果您使用多重继承而不是组合来解决这些问题,那么您就做错了.
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 来限定继承可避免上述双重布局,但无论如何,根据我的经验,您可能做错了什么......
在对象层次结构中,您应该尝试将层次结构保持为树(一个节点有一个父节点),而不是图形.
关于钻石的更多信息(编辑 2017-05-03)
C++ 中恐惧钻石的真正问题(假设设计是合理的 - 检查您的代码!),是您需要做出选择:
- 是否希望
A
类在您的布局中出现两次,这意味着什么?如果是,那么一定要从它继承两次. - 如果它应该只存在一次,那么虚拟地继承它.
这种选择是问题所固有的,而且在 C++ 中,与其他语言不同,您实际上可以做到这一点,而无需教条在语言级别强制您的设计.
但与所有权力一样,权力也伴随着责任:审查您的设计.
3.接口
零个或一个具体类的多重继承,以及零个或多个接口通常是可以的,因为您不会遇到上述的恐惧钻石.事实上,这就是 Java 的工作方式.
通常,当 C 继承自 A
和 B
时,您的意思是用户可以像使用 一样使用
,和/或好像它是一个 C
AB
.
在 C++ 中,接口是一个抽象类,它具有:
其所有方法声明为纯虚拟(后缀= 0)(删除了2017-05-03)- 无成员变量
零到一个真实对象的多重继承,以及零个或多个接口不被认为是臭的";(至少,没有那么多).
有关 C++ 抽象接口的更多信息(编辑 2017-05-03)
首先,NVI 模式可用于生成接口,因为真正的标准是没有状态(即没有成员变量,除了this
).你的抽象接口的重点是发布一个契约(你可以这样称呼我,这样称呼我"),仅此而已.只有抽象虚方法的限制应该是一种设计选择,而不是一种义务.
其次,在 C++ 中,从抽象接口虚拟继承是有意义的(即使有额外的成本/间接).如果您不这样做,并且接口继承在您的层次结构中多次出现,那么您就会有歧义.
第三,面向对象很棒,但它不是 C++ 中的唯一的真相TM.使用正确的工具,并始终记住您在 C++ 中还有其他范例可提供不同类型的解决方案.
4.你真的需要多重继承吗?
有时是的.
通常,您的 C
类继承自 A
和 B
,以及 A
和 B
是两个不相关的对象(即不在同一个层次结构中、没有共同点、不同的概念等).
例如,您可以有一个带有 X、Y、Z 坐标的 Nodes
系统,能够进行大量几何计算(可能是一个点,几何对象的一部分),并且每个 Node 是一个自动代理,能够与其他代理通信.
也许您已经可以访问两个库,每个库都有自己的命名空间(使用命名空间的另一个原因……但是您使用命名空间,不是吗?),一个是 geo
和其他是 ai
所以你有自己的 own::Node
派生自 ai::Agent
和 geo::Point
.
此时您应该问问自己是否不应该使用组合来代替.如果 own::Node
真的既是 ai::Agent
又是 geo::Point
,那么组合就行不通了.>
然后你需要多重继承,让你的 own::Node
根据他们在 3D 空间中的位置与其他代理通信.
(你会注意到 ai::Agent
和 geo::Point
是完全、完全、完全无关的……这大大降低了危险多重继承)
其他情况(编辑 2017-05-03)
还有其他情况:
- 使用(希望是私有的)继承作为实现细节
- 一些像策略这样的 C++ 习语可以使用多重继承(当每个部分需要通过
this
与其他部分进行通信时) - 来自 std::exception 的虚拟继承(异常是否需要虚拟继承?)
- 等
有时你可以使用组合,有时 MI 更好.重点是:你有一个选择.负责任地进行(并审查您的代码).
5.那么,我应该做多重继承吗?
大多数时候,根据我的经验,没有.MI 不是正确的工具,即使它看起来有效,因为它可以被懒惰的人使用而没有意识到后果(例如使 Car
同时成为 Engine
和 Wheel
).
但有时,是的.那时,没有什么比 MI 更有效的了.
但是因为 MI 很臭,所以准备在代码审查中捍卫你的架构(捍卫它是一件好事,因为如果你无法捍卫它,那么你就不应该这样做).
Is it a good concept to use multiple inheritance or can I do other things instead?
解决方案Multiple inheritance (abbreviated as MI) smells, which means that usually, it was done for bad reasons, and it will blow back in the face of the maintainer.
Summary
- Consider composition of features, instead of inheritance
- Be wary of the Diamond of Dread
- Consider inheritance of multiple interfaces instead of objects
- Sometimes, Multiple Inheritance is the right thing. If it is, then use it.
- Be prepared to defend your multiple-inherited architecture in code reviews
1. Perhaps composition?
This is true for inheritance, and so, it's even more true for multiple inheritance.
Does your object really needs to inherit from another? A Car
does not need to inherit from an Engine
to work, nor from a Wheel
. A Car
has an Engine
and four Wheel
.
If you use multiple inheritance to resolve these problems instead of composition, then you've done something wrong.
2. The Diamond of Dread
Usually, you have a class A
, then B
and C
both inherit from A
. And (don't ask me why) someone then decides that D
must inherit both from B
and C
.
I've encountered this kind of problem twice in 8 eights years, and it is amusing to see because of:
- How much of a mistake it was from the beginning (In both cases,
D
should not have inherited from bothB
andC
), because this was bad architecture (in fact,C
should not have existed at all...) - How much maintainers were paying for that, because in C++, the parent class
A
was present twice in its grandchild classD
, and thus, updating one parent fieldA::field
meant either updating it twice (throughB::field
andC::field
), or having something go silently wrong and crash, later (new a pointer inB::field
, and deleteC::field
...)
Using the keyword virtual in C++ to qualify the inheritance avoids the double layout described above if this is not what you want, but anyway, in my experience, you're probably doing something wrong...
In Object hierarchy, you should try to keep the hierarchy as a Tree (a node has ONE parent), not as a graph.
More about the Diamond (edit 2017-05-03)
The real problem with the Diamond of Dread in C++ (assuming the design is sound - have your code reviewed!), is that you need to make a choice:
- Is it desirable for the class
A
to exist twice in your layout, and what does it mean? If yes, then by all means inherit from it twice. - if it should exist only once, then inherit from it virtually.
This choice is inherent to the problem, and in C++, unlike other languages, you can actually do it without dogma forcing your design at language level.
But like all powers, with that power comes responsibility: Have your design reviewed.
3. Interfaces
Multiple inheritance of zero or one concrete classes, and zero or more interfaces is usually Okay, because you won't encounter the Diamond of Dread described above. In fact, this is how things are done in Java.
Usually, what you mean when C inherits from A
and B
is that users can use C
as if it was a A
, and/or as if it was a B
.
In C++, an interface is an abstract class which has:
all its method declared pure virtual (suffixed by = 0)(removed the 2017-05-03)- no member variables
The Multiple inheritance of zero to one real object, and zero or more interfaces is not considered "smelly" (at least, not as much).
More about the C++ Abstract Interface (edit 2017-05-03)
First, the NVI pattern can be used to produce an interface, because the real criteria is to have no state (i.e. no member variables, except this
). Your abstract interface's point is to publish a contract ("you can call me this way, and this way"), nothing more, nothing less. The limitation of having only abstract virtual method should be a design choice, not an obligation.
Second, in C++, it makes sense to inherit virtually from abstract interfaces, (even with the additional cost/indirection). If you don't, and the interface inheritance appears multiple time in your hierarchy, then you'll have ambiguities.
Third, object orientation is great, but it is not The Only Truth Out ThereTM in C++. Use the right tools, and always remember you have other paradigms in C++ offering different kind of solutions.
4. Do you really need Multiple Inheritance?
Sometimes, yes.
Usually, your C
class is inheriting from A
and B
, and A
and B
are two unrelated objects (i.e. not in the same hierarchy, nothing in common, different concepts, etc.).
For example, you could have a system of Nodes
with X,Y,Z coordinates, able to do a lot of geometric calculations (perhaps a point, part of geometric objects) and each Node is an Automated Agent, able to communicate with other agents.
Perhaps you already have access to two libraries, each with its own namespace (another reason to use namespaces... But you use namespaces, don't you?), one being geo
and the other being ai
So you have your own own::Node
derive both from ai::Agent
and geo::Point
.
This is the moment when you should ask yourself if you should not use composition instead. If own::Node
is really really both a ai::Agent
and a geo::Point
, then composition will not do.
Then you'll need multiple inheritance, having your own::Node
communicate with other agents according to their position in a 3D space.
(You'll note that ai::Agent
and geo::Point
are completely, totally, fully UNRELATED... This drastically reduces the danger of multiple inheritance)
Other cases (edit 2017-05-03)
There are other cases:
- using (hopefully private) inheritance as implementation detail
- some C++ idioms like policies could use multiple inheritance (when each part needs to communicate with the others through
this
) - the virtual inheritance from std::exception (Is Virtual Inheritance necessary for Exceptions?)
- etc.
Sometimes you can use composition, and sometimes MI is better. The point is: You have a choice. Do it responsibly (and have your code reviewed).
5. So, should I do Multiple Inheritance?
Most of the time, in my experience, no. MI is not the right tool, even if it seems to work, because it can be used by the lazy to pile features together without realizing the consequences (like making a Car
both an Engine
and a Wheel
).
But sometimes, yes. And at that time, nothing will work better than MI.
But because MI is smelly, be prepared to defend your architecture in code reviews (and defending it is a good thing, because if you're not able to defend it, then you should not do it).
相关文章