Before
C++, the most successful object-oriented language was Smalltalk.
Smalltalk was created from the ground up as an OO language. It is often
referred to as
pure,
whereas C++, because it was built on top of C, is called
hybrid.
One of the design decisions made with Smalltalk was that all classes would be
derived in a single hierarchy, rooted in a single base class (called
Object
– this is the model for the
object-based
hierarchy).
You cannot create a new class in Smalltalk without inheriting it from an
existing class, which is why it takes a certain amount of time to become
productive in Smalltalk – you must learn the class library before you can
start making new classes. So the Smalltalk class hierarchy is always a single
monolithic tree.
Classes
in Smalltalk usually have a number of things in common, and always have
some
things in common (the characteristics and behaviors of
Object),
so you almost never run into a situation where you need to inherit from more
than one base class. However, with C++ you can create as many hierarchy trees
as you want. Therefore, for logical completeness the language must be able to
combine more than one class at a time – thus the need for multiple
inheritance.
However,
this was not a crystal-clear case of a feature that no one could live without,
and there was (and still is) a lot of disagreement about whether MI is really
essential in C++. MI was added in AT&T
cfront
release 2.0 and was the first significant change to the language. Since then, a
number of other features have been added (notably templates) that change the
way we think about programming and place MI in a much less important role. You
can think of MI as a “minor” language feature that shouldn’t
be involved in your daily design decisions.
One
of the most pressing issues that drove MI involved containers. Suppose you want
to create a container that everyone can easily use. One approach is to use
void*
as the type inside the container, as with
PStash
and
Stack.
The Smalltalk approach, however, is to make a container that holds
Objects.
(Remember that
Object
is the base type of the entire Smalltalk hierarchy.) Because everything in
Smalltalk is ultimately derived from
Object,
any container that holds
Objects
can hold anything, so this approach works nicely.
Now
consider the situation in C++. Suppose vendor
A
creates an object-based hierarchy that includes a useful set of containers
including one you want to use called
Holder.
Now you come across vendor
B’s
class hierarchy that contains some other class that is important to you, a
BitImage
class, for example, which holds graphic images. The only way to make a
Holder
of
BitImages
is to inherit a new class from both
Object,
so it can be held in the
Holder,
and
BitImage:
This
was seen as an important reason for MI, and a number of class libraries were
built on this model. However, as you saw in Chapter 14, the addition of
templates has changed the way containers are created, so this situation
isn’t a driving issue for MI.
The
other reason you may need MI is logical, related to design. Unlike the above
situation, where you don’t have control of the base classes, in this one
you do, and you intentionally use MI to make the design more flexible or
useful. (At least, you may believe this to be the case.) An example of this is
in the original iostream library design:
Both
istream
and
ostream
are useful classes by themselves, but they can also be inherited into a class
that combines both their characteristics and behaviors.
Regardless
of what motivates you to use MI, a number of problems arise in the process, and
you need to understand them to use it.