Borland Online And The Cobb Group Present:


December, 1995 - Vol. 2 No. 12

C++ Language basics - Using multiple inheritance effectively

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, and­­just as important­­that you know when you shouldn't use multiple inheritance.

Preparing for 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.

Public

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.

Private

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.

Protected

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.

The compiler's view of inheritance

From the compiler's point of view, three things happen when you derive a class from a base class:

The inherited sub-object

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.

Overriding virtual functions

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 vptr­­a pointer to the HouseVtable­­inside 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.

Implicit conversions

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.

The class designer's view of inheritance

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.

Layering­­adding the sub-object

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.

Private or protected inheritance­­overriding virtual 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.

Public inheritance­­allowing implicit conversions

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).

Conclusion

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.

Return to the Borland C++ Developer's Journal index

Subscribe to the Borland C++ Developer's Journal


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.