When virtual inheritance IS a good design?
If you have an interface hierarchy and a corresponding implementation hierarchy, making the interface base classes virtual bases is necessary.
E.g.
struct IBasicInterface
{
virtual ~IBasicInterface() {}
virtual void f() = 0;
};
struct IExtendedInterface : virtual IBasicInterface
{
virtual ~IExtendedInterface() {}
virtual void g() = 0;
};
// One possible implementation strategy
struct CBasicImpl : virtual IBasicInterface
{
virtual ~CBasicImpl() {}
virtual void f();
};
struct CExtendedImpl : virtual IExtendedInterface, CBasicImpl
{
virtual ~CExtendedImpl() {}
virtual void g();
};
Usually this only makes sense if you have a number of interfaces that extend the basic interface and more than one implementation strategy required in different situations. This way you have a clear interface hierarchy and your implementation hierarchies can use inheritance to avoid the duplication of common implementations. If you're using Visual Studio you get a lot of warning C4250, though.
To prevent accidental slicing it is usually best if the CBasicImpl
and CExtendedImpl
classes aren't instantiable but instead have a further level of inheritance providing no extra functionality save a constructor.
Virtual inheritance is a good design choice for the case when a class A extends another class B, but B has no virtual member functions other than possibly the destructor. You can think of classes like B as mixins, where a type hierarchy needs only one base class of the mixin type in order to benefit from it.
One good example is the virtual inheritance that is used with some of the iostream templates in the libstdc++ implementation of the STL. For example, libstdc++ declares template basic_istream
with:
template<typename _CharT, typename _Traits>
class basic_istream : virtual public basic_ios<_CharT, _Traits>
It uses virtual inheritance to extend basic_ios<_CharT, _Traits>
because istreams should only have one input streambuf, and many operations of an istream should always have the same functionality (notably the rdbuf
member function to get the one and only input streambuf).
Now imagine that you write a class (baz_reader
) that extends std::istream
with a member function to read in objects of type baz
, and another class (bat_reader
) that extends std::istream
with a member function to read in objects of type bat
. You can have a class that extends both baz_reader
and bat_reader
. If virtual inheritance were not used, then the baz_reader
and bat_reader
bases would each have their own input streambuf—probably not the intent. You would probably want the baz_reader
and bat_reader
bases to both read from the same streambuf. Without virtual inheritance in std::istream
to extend std::basic_ios<char>
, you could accomplish that by setting the member readbufs of the baz_reader
and bat_reader
bases to the same streambuf object, but then you would have two copies of the pointer to the streambuf when one would suffice.
Grrr .. Virtual inheritance MUST be used for abstraction subtyping. There is utterly no choice if you are to obey the design principles of OO. Failing to do so prevents other programmers deriving other subtypes.
An abstract example first: you have some base abstraction A. You want to make a subtype B. Please note subtype necessarily means another abstraction. If it isn't abstract, it is an implementation not a type.
Now another programmer comes along and wants to make a subtype C of A. Cool.
Finally, yet another programmer comes along and wants something which is both a B and a C. It's also an A of course. In these scenarios virtual inheritance is mandatory.
Here's a real world example: from a compiler, modelling data types:
struct function { ..
struct int_to_float_type : virtual function { ..
struct cloneable : virtual function { ..
struct cloneable_int_to_float_type :
virtual function,
virtual int_to_float_type
virtual cloneable
{ ..
struct function_f : cloneable_int_to_float_type {
Here, function
represents functions, int_to_float_type
represents a subtype
consisting of functions from int to float. Cloneable
is a special property
that the function can be cloned. function_f
is a concrete (non-abstract)
function.
Note that if I did not originally make function
a virtual base of int_to_float_type
I could not mixin cloneable
(and vice versa).
In general, if you follow "strict" OOP style, you always define a lattice of abstractions, and then implementations are derived for them. You separate strictly subtyping which only applies to abstractions, and implementation.
In Java, this is enforced (interfaces are not classes). In C++ it isn't enforced, and you don't have to follow the pattern, but you should be aware of it, and the larger the team you're working with, or project you're working on, the stronger the reason you will need to depart from it.
Mixin typing requires a lot of housekeeping in C++. In Ocaml, classes and class types are independent and matched by structure (possession of methods or not) so inheritance is always a convenience. This is actually much easier to use than nominal typing. Mixins provide a way to simulate structural typing in a language that only has nominal typing.