I think people just don't understand why multiple inheritance is dangerous - and no wonder. "Multiple inheritance is bad" and "single inheritance is good" is common delusion (note: style guide authors don't share this delusion), but reality is significantly more complex.
C++ mixes two totally unrelated things: interface inheritance (which existed half-century ago under different name because it's essential for most major projects) and implementation inheritance. Worse: it makes safe "extensionable" implementation inheritance very similarly looking to it's extremely dangerous evil twin.
Interface inheritance is *always* safe. Single-inheritance, multiple inheritance - it's all well and good.
Implementation inheritance combined with virtual functions, on the other hand, is goto++: it's more powerful than goto and it's more dangerous then goto. Multiple inheritance in conjunction with the implementation inheritance with virtual functions (which is what the example which started this thread contained) is unmitigable disaster.
Everyone knows the catchphrase "Go To Statement Considered Harmful" and often people try to avoid goto like a plague. But how many of you have actually READ the aforementioned Edsger Dijkstra's article and compared the reasons for why goto is considered harmful with the reasons for why implementation inheritance is considered harmful? I'll cite: "My ... remark is that our intellectual powers are rather geared to master static relationships and that our powers to visualize processes evolving in time are relatively poorly developed. For that reason we should do (as wise programmers aware of our limitations) our utmost best to shorten the conceptual gap between the static program and the dynamic process, to make the correspondence between the program (spread out in text space) and the process (spread out in time) as trivial as possible".
Well, the implementation inheritance can (and usually does!) play the same role as goto. Consider a simple example:
class Base {
void foo(void) {
...
bar();
...
}
virtual void bar(void) {
...
}
}
What have you just did? Well, you've inserted RUNTIME-SWITCHABLE GOTO STATEMENT right in the middle of the instruction foo()! Now you can only understand what the function foo for the particular descendant of the class Base is doing if you'll take very detailed look on the implementation of the bar() function in said descendant (and of course in most cases foo() authors will just assume that bar() descendant will do "something similar to what Base::bar is doing"). In a sense implementation of bar is precondition of the function foo() - which is often not even mentioned in the description of function foo()! Or let's consider even more innocuous class:
class Base {
virtual void foo(void) {
...
}
virtual void bar(void) {
...
}
}
This class looks innocent enough, but in reality it's descendants can easily tie foo() and bar() in the unimaginable knots because now you have TWO slots in vtable which play the role of RUNTIME-SWITCHABLE GOTO STATEMENTS.
Compare with interface inheritance. If all the functions are pure virtual then the class which implements these functions is fully autonomous: they can be considered in isolation and surprising jumps from one function to another (or even less surprising lack of jumps) can not longer happen. But if you will then EXTEND this class... then suddenly you back in the square one WRT dreaded RUNTIME-SWITCHABLE GOTO STATEMENT. C++11 solves this problem with it's "final" syntax, but in C++98 there are no mitigation.
Note the first example: if bar() there is pure virtual (and is only ever overloaded once) then you essentially have the same "no-surprises" property even if, formally speaking, you are doing implementation inheritance.
Yup. As style guide says: composition is often more appropriate than inheritance. Implementation inheritance is modern version of goto and while we don't ban goto (yes, there are no such rule in style guide) we know that we should use it rarely and sparingly. Yes implementation inheritance (including very dangerous multiple implementation inheritance) is used as if it's the most natural thing to do.
I guess it's because of the unfortunate property of C++ which mixes important and indispensable interface inheritance with dangerous and fragile implementation inheritance, but maybe we can be a liiitle more attentive?