by Tim Gooch
This article is the second in a three-part series. It has been
adapted from "Using Multiple Inheritance Effectively"
in Black Belt C++ (MIS:Press, 1994), (800) 628-9658.
In general, you should think of multiple inheritance as a two-edged sword. The best part about multiple inheritance is that you can derive a new class from more than one base class. On the other hand, the worst part about multiple inheritance is... that you can derive a new class from more than one base class.
Last month, we reviewed the basics of single inheritance and of
using the public, protected, and private
derivation specifiers when you derive a new class from an existing
class. In this article, we'll look at some issues you'll
want to keep in mind when you're considering whether to
use multiple inheritance in a particular situation.
Back at our hypothetical consumer electronics company, the designers
have decided that they need to create a new version of their best-selling
boom box that will also play CDs. (This is not exactly a leading-edge
company.) In all likelihood, this company has the technical expertise
to design and add the CD hardware to the new boom box. In fact,
elsewhere in the company, they manufacture a home CD player. Unfortunately,
adding the types of components to the boom box that exist in
the home unit will require a considerable investment in time from
the in-house CD expert.
To get the product to market quickly, the company could simply manufacture a sub-frame for the boom box that would hold the home CD player. By adding a power inverter (to convert the 15 volts DC from the batteries to 110 AC) and an RCA cable (to route the CD output to the Auxiliary input), the company can now offer a portable boom box CD system. The purchase price for the unit will be high (home CD + boom box + sub-frame + power inverter + cable), and the battery life will be short, but they'll be able to get the product on the market quickly.
If by now you think the company has lost its collective mind, don't worry. They haven't yet tried to ship a product using this design. However, programmers take notethis design is not that much different from some software designs that use layering to reuse code.
While this may seem to be an acceptable way to reuse behavior from an existing class, it's important for you to remember that your new class will be carrying the "baggage" of the layered object. If the additional overhead of the additional object is slight, there's a good chance for this approach to work. Unfortunately, layering brings with it some serious limitations.
For example, imagine that our electronics company wants to incorporate some form of synchronized recording between the CD subsystem and the boom box's cassette recorder. What the marketing department wants is this: If you set the cassette system to record and then press the Pause button, pressing the Play button on the CD control panel will cause the CD to start playing and the cassette system to start recording.
In object-oriented terms, the marketing department has just asked us to override a function of the CD player. Unfortunately, if we simply reuse the existing electronics and logic circuitry and encapsulate it in a different case, we won't be able to intercept the proper signalsin effect, the cassette recorder and the CD player are still two separate machines. In the same way, if you use layering, you won't be able to intercept internal function calls that implement the enclosed object's behavior.
As with the CD player, if the object you're encapsulating
doesn't do much, the potential for using layering is much
stronger. However, most companies don't spend a great deal
of time creating products that aren't useful by themselves,
and accordingly they haven't planned to reuse such products.
If instead of adding new controls, the new CD boom box retains all of the controls of the old boom box, users will know how to use the CD version right away (except for the new CD-specific controls). If the company decides to add CD capability by including only those CD hardware components that are appropriate for portable use, the price will go down and the battery life will go up. On the downside, this approach may require extensive testing since the CD section of the boom box is essentially a new product.
Creating the new CD boom box this way is similar to deriving a
new class using public inheritance and then adding only
the new members we need for additional functionality. We'll
call this parasitic inheritance since the derived class
members become parasites to those of the base class (they attach
to the base class object and cannot exist as a unit without it).
In another development, one of the design engineers has noticed that personal CD players themselves are becoming popular items. The engineer recommends that the company design a CD player chassis that would be suitable for a portable CD system in addition to providing the CD capabilities for the new model boom box and for a redesigned version of the home CD player.
What the engineer has done is a very important technique in considering
whether to apply multiple inheritance in a specific software solution.
First, the engineer recognized that different pieces in the existing
product line exhibited common behavior and attributes. Second,
this person identified new opportunities to use this common behavior-and-attribute
set.
Children think nothing of asking hundreds of questions every day
(sometimes about more than one topic). In general, asking questions
is also a good practice when you're analyzing a problem
and designing a software solution. So let's look at some
questions you can ask when you're considering using multiple
inheritance. Then, we'll look at some common situations
in which you may want to use multiple inheritance.
One of the first points you'll want to consider is how the program will use the different types of objects. If the program will need to keep homogeneous lists or collections of objects that have different capabilities but share some fundamental behavior or attributes, you'll probably want to consider deriving the different object types from a common base class. Another way of looking at this question is by asking yourself whether the program will need to convert a pointer or reference to a given type of object into a pointer or reference to a base class object. If the program will need to perform this type of conversion, you need to specify that one class inherits from the other.
The next point you should consider is whether some of these objects
will need to appear in more than one list or collection. If the
answer is yes, the program may need to perform implicit conversions
to more than one base class. Allowing multiple implicit conversions
is one of the most obvious benefits of multiple inheritance. Finally,
make sure you ask the question, Will some of these objects need
to override virtual functions from more than one class? If the
answer is yes, you should definitely consider multiple inheritance.
When you first begin identifying classes within an application's problem domain, look for common behavior/attribute combinationseven in unrelated parts of the program. These behavior/attribute combinations may be suitable candidates to become add-in classes. In an inheritance graph, you can picture an add-in class as one that suddenly appears off to the side of one or more of the classes and then simply "hangs" on the existing class's behavior and attributes.
Unfortunately, to know what to look for, you'll have to understand the problem domain thoroughly. For example, when the engineer at the boom box company recognized that the company might soon be manufacturing two products that both played music from CDs, the engineer immediately found a third possible product that could use this sub-assembly, because he or she knew the market where the company was trying to sell products. The engineer also had enough technical knowledge of sub-assemblies and CD hardware to know that the third product was a practical possibility.
Whenever you're about to consider a base/derived relationship between two classes, take a close look at how the derived class affects the behavior or attributes of the base class. Does the derived class change the behavior and attributes of the base class, or does it merely add new ones (parasitic inheritance)? If some of the derived class members don't change the base class behavior or communicate with the base class in any way, you may want to consider moving those members into a separate class and then deriving a new class from that class and the existing base, using multiple inheritance.
For example, if you were creating an employee payroll application,
and you already had a Person class that defined the usual
name and address data, you could derive a new Employee
class from this class using something like the following code:
class Employee : public Person { public: Employee(unsigned long num); void PrintEmployeeNum();// Not a function in the // base class private: unsigned long EmployeeNumber; };
The data and the member function we define in this class are purely
supplemental to the Person class. Instead, we could create
another class this way:
class EmployeeData { public: EmployeeData(unsigned long num); void PrintEmployeeNum(); private: unsigned long EmployeeNumber; };
Then, we could derive the Employee class:
class Employee : public Person, public EmployeeData { public: Employee(unsigned long num); };
There are two primary advantages to moving code to an add-in class this way. First, we'll be able to debug and refine the EmployeeData class as a unit. Second, we'll be able to later use the EmployeeData class to create other non-Person employee classes (for example, a SubContractorCompany class). Third, if there are parts of the program that don't need to use anything other than members of the EmployeeData class, we can refer to Employee objects via an EmployeeData pointer or reference. This will help communicate what that section of the program is doing with those objects, and it will allow us to hold pointers or references to any EmployeeData derived class (for example, Employee or SubContractorCompany) in a list of EmployeeData objects.
Another excellent time to use add-in classes occurs when you're
implementing reference counting and smart pointers to clean up
memory allocation problems. You can create a class that maintains
the reference count for any derived class. For example, given
a class Window, you could use this code:
class Window { // details here }; class RefCount { friend class SmartPointer; protected: unsigned int _rc; }; class RefCountWin : public Window, private RefCount { // duplicate Window class constructors here };
Now, objects of the SmartPointer class can increment
and decrement the reference count _rc for a given RefCountWin
object as necessary. In addition, you could keep a list of RefCount
objects (possibly as a static member of the RefCount
class) and perform debugging or reporting operations on all of
the reference-counted objects in the program at a given time.
In this situation, it doesn't really matter whether you
created the Window class or whether it's part
of an off-the-shelf class library. As long as there aren't
any naming conflicts between the single member _rc and
members of the other base classes, you won't have any problems.
If you're developing a relatively small class hierarchy
where you know the full set of services that the derived classes
will need to provide, creating a mix-in hierarchy may
be acceptable. For example, examine the following classes:
class ThreeCourseMeal { public: virtual Food GetFirstCourse() = 0; virtual Food GetSecondCourse() = 0; virtual Food GetThirdCourse() = 0; void Eat() { cout << GetFirstCourse() << endl; cout << GetSecondCourse() << endl; cout << GetThirdCourse() << endl; } }; class MexicanAppetizer : virtual public ThreeCourseMeal { public: char* GetFirstCourse() { return "Nachos"; } }; class MexicanMainDish : virtual public ThreeCourseMeal { public: char* GetSecondCourse() { return "Burrito"; } }; class MexicanDessert : virtual public ThreeCourseMeal { public: char* GetThirdCourse() { return "Fried Ice Cream"; } }; class MexicanThreeCourseMeal : public MexicanAppetizer, public MexicanMainDish, public MexicanDessert { };
By specifying the full set of members in the base class ThreeCourseMeal, you've communicated what the derived classes have to do. It doesn't matter which classes implement the different functions, as long as you implement them somewhere before you try to create an object of that type. When you're designing one of the "mixed-in" classes (one of the three courses), you'll know what members the other classes are going to have to define. Figure A shows the inheritance hierarchy along with a diagram that depicts which classes implement which functions.
Figure A - If you know the full set of services your classes will implement, you may want to create a mix-in hierarchy.
If you like, you can specify that you can call the public functions of the mix-in base class only from a pointer of the mix-in base type. To do this, you'd simply declare the overriding functions in the intermediate classes' protected sections. A mix-in base class pointer will be able to call these functions since they're public in that class. However, the virtual function mechanism will ensure that the compiler calls the correct version of the functions at runtime. Also, a mix-in hierarchy will usually define or predict how you'll combine definitions of the base class members in the intermediate classes. In the example above, you should always define an intermediate class as one that defines just one course of the meal. Limiting the intermediate classes this way will allow you to confidently combine ChineseAppetizer, FrenchMainDish, and ItalianDessert classes into a PotpourriThreeCourseMeal class without worrying about one intermediate class implementing the same functions as another.
A variation on this form involves implementing a default version
of each function in the base class. For example, if we changed
the base class declaration to
class ThreeCourseMeal { public: virtual char* GetFirstCourse() { return "Water"; } virtual char* GetSecondCourse() { return "Water"; } virtual char* GetThirdCourse() { return "After-dinner Mint"; } void Eat() { // same as before } };
we could then define a class like this:
class MexicanFastFood : public MexicanMainDish, public MexicanDessert { };
If you think about it, you'll see that there are now two paths in the inheritance hierarchy for each of the GetSecondCourse() and GetThirdCourse() functions (the base class version and derived class version of each). Graphically, you can see the paths for the GetSecondCourse() function in the MexicanFastFood class in Figure B.
Figure B - If you create mix-in classes, you may create multiple virtual function paths.
In C++, the dominance rule resolves this ambiguity (if
you're using virtual base classes). Because the MexicanMainDish
derived class "knowingly" provides a new version
of the GetSecondCourse() function, that's the
one the compiler will call. (The author of the class should know
he or she is either hiding a non-virtual function of the same
name, or overriding a virtual function from the base class). Since
the compiler assumes you defined the function this way intentionally,
it also assumes that this is the version you'll want to
call, instead of the base class version that's available
through an alternate path.
You can use multiple inheritance to solve many real-world programming
problems and to help realistically model elements of a problem
domain. In addition, you can apply multiple inheritance in several
ways (add-in classes, mix-in classes, and so on). Next month,
we'll finish this series by reviewing some of the obstacles
you may face when you begin applying 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.