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 both B
and C
), 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 class D
, and thus, updating one parent field A::field
meant either updating it twice (through B::field
and C::field
), or having something go silently wrong and crash, later (new a pointer in B::field
, and delete C::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).
Best Solution
But you need to know how C++ organises memory. The layout of a class, CClass, is as follows:
OK, it's a bit more complex than that if there is an inheritance tree, but you should get the basic idea. Assuming an int is four bytes then class C is laid out like:
But the an object of type C is all of the above, so a pointer to a C is pointing to offset 0. However, C++ allows implicit downcasting, so a pointer to a C can be converted to a pointer to an A or a pointer to a B. But if you look at the above, a pointer to a B is at offset 8 rather than 0 (a pointer to a C) and that a pointer to an A is at offset 0. So, casting a pointer to a C to a pointer to a B adds 8 to the pointer value. This is because the methods of B assume the 'this' pointer points to B's first member (B::xx), but a pointer to a C if reinterpreted as a pointer to a B (i.e. the value is the same) would be pointer to an address eight bytes before where B actually is, so that all of B's methods would be using, in this instance, A's members.
Upcasting (the final two conversions) is different kettle of fish. Going from a pointer to a B to a pointer to a C is really hard because you don't know if the pointer to a B is just pointing to an instance of B or at an instance of C plus eight. This is where RTTI and the dynamic cast comes in. With RTTI (Run Time Type Information) enabled, the pointer to B contains additional information that describes what B really is - a simple B or a B as part of a C. This does have additional cost, both execution time and memory usage.
Finally, this does highlight the ambiguity of the C style cast. You really should use the C++ style casts (static_cast <>, etc) as this clarifies the way the conversion should be done.
*This could also be a yes, I guess it depends on the compiler and if RTTI is on or off. You would need to get into the details of the standard and the compiler implementation to say for sure.