December, 1995 - Vol. 2 No. 12
By Tim Gooch
This article is the first in a series. It has been adapted
from "Using Multiple Inheritance Effectively" in
Black Belt C++, (MIS:Press, 1994), (800)
628-9658.
One of the more controversial elements of C++ is multiple inheritance (deriving a class from more than one base class). Many of the newer object-oriented languages support multiple inheritance, while several of the traditional (some would say "pure") object-oriented languages such as Smalltalk, Object Pascal, and Objective C don't. Obviously, the language-designing community doesn't agree on the value of this feature.
However, whether it's "better" for C++ to have multiple inheritance or not is no longer an issue, because it's already part of C++. (You'll notice that this article isn't titled "Avoiding Multiple Inheritance.") We'll look at some of the issues related to taking advantage of multiple inheritance with C++.
This month, we'll review the basics of single inheritance, since an incomplete understanding of its issues can further complicate understanding multiple inheritance. Next month, we'll look at some of the issues you should consider when you're trying to decide whether multiple inheritance is an appropriate solution to a design or implementation problem. With this in mind, we'll look at methods of analyzing the problem domain to determine whether multiple inheritance could be a valuable component of the solution. Along the way, we'll examine some successful design techniques that use multiple inheritance.
In the final installment, we'll look at some of the more
troublesome mechanics of using multiple inheritance in real applications.
While this is the aspect that many discussions of multiple inheritance
focus on the most, we can avoid a fair amount of unnecessary detail
by ensuring that you fully understand inheritance in the first
place, andjust as importantthat you know when
you shouldn't use multiple inheritance.
In order to use multiple inheritance in C++, you first need to
understand single inheritance in C++. There are at least two ways
you can view inheritance in C++from the compiler's
perspective and from the designer's perspective. First,
let's review some class terminology. In any class or struct,
you can specify that data members or member functions exist within
one of three access levels: public, private,
or protected.
Data members or member functions are public if they are
visible to any part of the application that has a valid identifier
for an object of this class. Many people refer to public
members as the interface to a class, since most of the
surrounding application will use these members to communicate
with an object of that class.
Data members or member functions are private if they
aren't visible to the world outside this class's
member functions and friends. Many people refer to
private members as the implementation of
the class, since member functions or friends of this class are
the only parts of the application that can use these members.
If you place the implementation details of the class in private
members, you're using implementation hiding.
Data members or member functions are protected if they
aren't visible to the world outside this class, but they
are visible to classes you derive from this class and to derived
member functions and friends. If you derive a new class from an
existing one, the new class won't have access to the base
class's private members, but it will have access
to its public and protected members.
If you like to think of private members as the implementation
of the class, think of protected members
as the implementation of this class and any derived classes.
From the compiler's point of view, three things happen
when you derive a class from a base class:
The most obvious aspect of inheritance is that the derived class "inherits" the members of the base class. In general, you can think of the base class members as composing a complete, unnamed sub-object of the base class inside each derived class object.
For example, assume that you've defined a struct, House,
struct House { char Style[20]; int NumOfRooms; };
A typical C++ compiler might create a memory image of a House
object that looks like this:
-------------------------- | Style | // 20 bytes -------------------------- | NumOfRooms | // 4 bytes --------------------------
Now, if you derive a new struct, HouseWithAddition, from
the House base struct by writing
struct HouseWithAddition : public House { char Addition[20]; };
the compiler might create a memory image of a HouseWithAddition
object that looks like this:
-------------------------- | Style | // 20 bytes -------------------------- | NumOfRooms | // 4 bytes -------------------------- | Addition | // 20 bytes --------------------------
Without any complications, you've already "reused"
the data structure of the base struct. Each data member from the
House struct is as accessible as the new Addition
data member from the HouseWithAddition struct. Note that
this form of reuse is fundamentally different from the reuse you
were able to do in ANSI C. There, you would have had to write
typedef struct HouseWithAddition { struct House h; char Addition[20]; } HouseWithAdditionStruct;
and you would have been able to access the House data
member only via the named member, h. In C++, this is
analogous to
struct HouseWithAddition { struct House h; char Addition[20]; };
In each case, the memory image the compiler creates for a HouseWithAddition object would be almost identical. The difference lies in our ability to access the House class data members directly without using a named sub-object.
In the first declaration for the HouseWithAddition struct, you'll notice that we used the access specifier public in front of the name of the House base struct. The access specifier in front of a base type's name determines the access level for the sub-object's members. Table A shows how the different access specifiers affect the ability of the derived class's member functions to access a base class's members.
Table A: Derived class access
Derivation specifier | Public members | Protected members | Private members |
public | public | protected | (inaccessible) |
protected | protected | protected | (inaccessible) |
private | private | private | (inaccessible) |
For example, if you derive class B from class A using the protected
specifier, the public members of class A will become
protected members in class B. Likewise, if you use the
private specifier, the public and protected
members of class A will become private members in class
B. Members of class A that are private will never be
accessible in class B. Figure A
shows how using the different access specifiers affects the
access to inherited members of the sub-object.
Figure A - A derived class's derivation specifier determines the accessibility of the base class members.
The second characteristic of inheritance is that it allows the derived class to define new versions of base class virtual functions (this is known as overriding a virtual function). By overriding a virtual function, you allow the compiler to choose the correct virtual function from the derived class's vtable.
For example, if you specify a virtual function in the House class that you've named HeatAndCool(), the compiler will create a vtable for that class (we'll call it a HouseVtable). Each time you create a House object, the compiler embeds a vptra pointer to the HouseVtableinside the object. The compiler will then use the vptr and the vtable to look up the address of the function it should call.
When you derive the new HouseWithAddition class, the compiler will create another vtable for the House class functions if the HouseWithAddition class overrides any of the House class's virtual functions. However, the compiler will use this vtable only when it's calling a virtual function for the House sub-object in a HouseWithAddition object (we'll call this the HouseWithAdditionVtable). Inside the House sub-object of each HouseWithAddition object, the compiler will insert a pointer to the HouseWithAdditionVtable. If the HouseWithAddition class doesn't override the HeatAndCool() function, the compiler won't create the HouseWithAdditionVtable, and it will instead insert a pointer to the HouseVtable in the House sub-object.
Therefore, the content of the vtable for a sub-object
depends on the sub-object's class and its derived class
types. As you derive a class again and again, the compiler will
continue to create new vtables as necessary. In this
way, when you call a virtual function with an object of the derived
class, the vtable for the base sub-object will contain
the correct function address.
In C++, there are a number of ways the compiler can create an appropriate object when you use an object, pointer, or reference in a place where the compiler was expecting an object of a different type. In some cases, you'll want to specify that a conversion is possible. When you do this, you're creating a user-defined conversion.
In other cases, the compiler can create an object of the appropriate type by itself (for example, converting an int to a long int) by performing implicit conversions. When you derive one class from another, you're enabling the compiler to perform a number of implicit conversions.
Because the compiler will create a default copy constructor for each class where you don't specify one, a derived class will usually have some level of access (public, protected, or private) to the base class's copy constructor (unless the base class's copy constructor was a private member of the base class). This means the compiler will be able to use the base class's copy constructor to create a copy of the base class sub-object if it needs to convert a derived class object into a base class object.
For example, if you declared a new object of the HouseWithAddition
struct by writing
HouseWithAddition myHouse;
and you declared the function Print() as follows
void Print(House h)
calling this function with
Print(myHouse)
will cause the compiler to call the copy constructor for the House struct to create the temporary object the function needs. Unfortunately, the temporary House object has no way of knowing that it once was a part of a HouseWithAddition object. The object's true type (its class information) is lost. This is frequently referred to as "slicing," since the compiler "slices" off everything except the base type object.
In addition to allowing the compiler to convert objects (if the copy constructor is accessible), using inheritance gives the compiler permission to convert a derived class pointer or reference into a base class pointer or reference. However, these conversions don't occur because of a specific function such as the copy constructor. Therefore, you may be wondering whether the access specifiers for the base classes affect the compiler's ability to perform these conversions. In fact, they do. Instead of checking the accessibility of a particular function, the compiler allows implicit pointer or reference conversions only if the public members of the base class are accessible.
For example, if you use the public specifier when you
derive class B from class A, any part of the program will be able
to convert a pointer or reference to a B object into a pointer
or reference to an A object. If you use the protected
specifier (which makes the base class's public
members protected members of the derived class), only
member functions or friends of class B, or of classes you derive
from B, will be able to convert pointers or references to B objects
into pointers or references to A objects. Finally, using the private
specifier prevents any part of your program except member functions
or friends of class B from converting pointers or references to
B objects into pointers or references to A objects.
Now that we've seen what the compiler creates when we use inheritance, let's consider how the low-level issues affect design decisions. Since designers are used to thinking in terms other than the low-level data structures we were just looking at, let's get away from software design for a moment.
Consider a company that manufactures compact personal stereo equipment.
The initial model, which plays AM and FM radio in addition to
cassette tapes, is a huge success. They're planning a new
model that will automatically shut off the tape when it reaches
the end. Since it's unlikely that this company will create
the new design from scratch, let's look at their options
for reusing the previous model's design in the new model.
One solution may be to design a new cabinet that's just slightly larger than the existing model and then to place an old model boom box inside the case. By carefully positioning the openings and the new button assemblies, they might be able to make this approach work. (When the new model detects that the tape isn't moving, via a tape-motion sensor, it simulates a keypress to the Stop button.)
This bizarre scenerio is actually somewhat similar to including
a named sub-object in a new class to reuse code from an existing
class. The advantage to this approach is that it requires absolutely
no changes to the existing class, just as the new boom box design
reuses the old model "as is." The primary disadvantage
to using this approach is that the new class must replicate the
existing class's functions to be able to intercept any
of the member function calls destined for the sub-object. In addition,
functions inside the sub-object have no way of calling member
functions in the enclosing class (just as the existing boom box
components have no way of knowing about the external hardware).
If you instead use inheritance, you can let the derived class
implement a virtual function that you declared in the base class,
or you can call that function from other base class functions.
As an alternative to their first solution, the stereo company could use the above-mentioned new case, but instead of tacking on the additional hardware externally, they could add it inside the existing boom box. To do this, they'll need to have access to a number of signals and controls (for example, to electronically initiate the Stop function).
This solution is somewhat similar to using private inheritance in a software design. By deriving a new class privately from an existing class, you'll have an unnamed object of the existing class and the ability to intercept any of the existing class's functions and change those functions' behavior. Unfortunately, you'll still have to re-create the existing class's functions if you want them to appear in the new class.
One inheritance topic that's not discussed very often is the difference between private and protected inheritance. As we mentioned earlier, if you derive a new class using the protected specifier, public and protected members of the base class will become protected members of the derived class. However, if you derive another class from the original derived class, those members will still be accessible to the new class. Further derivations from here could continue to change the behavior of the protected base class.
If you use private inheritance, you'll be able to make changes to the private base class only in the first derived class. For further derivations, the private base class members won't be accessible. Consider using protected inheritance instead of private inheritance unless you don't want further derivations to change the private base class's behavior.
If you use private or protected inheritance,
you can change the visibility of a member by using an access
declaration. For example, if you privately derive class B
from class A, and class A contains a public member function
foo(), you can make A::foo() available in class
B by placing the fully qualified name A::foo() somewhere
in the declaration for class B. In this way, you can selectively
make some of the class A members part of the class B interface,
and you can make some of them part of the class B implementation.
Let's return one last time to our stereo example. Instead of using either of the above two solutions, the boom box company may decide to add the new hardware inside the existing boom box case design. Again, they'll need access to the internal signals and controls in the existing system.
This situation corresponds somewhat to public inheritance
in a software design. By inheriting a new class publicly from
an existing class, you'll have an unnamed sub-object of
the existing class, you'll have the ability to intercept
any of the existing class's functions and change their
behavior, and you'll have the same interface as the existing
class (so the compiler can perform implicit conversions).
Most C++ programmers haven't spent the time it takes to
really understand the full implications of using single inheritance.
Once they do, the mechanics of using multiple inheritance become
fairly straightforward. In the next several issues, we'll
show how you can use the basics of single inheritance, as we've
discussed them here, to intelligently add multiple inheritance
to your C++ programs.
Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.