Channels ▼
RSS

Using Multiple Inheritance in C++


SP92: USING MULTIPLE INHERITANCE IN C++

USING MULTIPLE INHERITANCE IN C++

When is it worth the effort?

Tom Cargill

Tom is a software consultant based in Boulder, Colorado, specializing in C++. He started programming in C++ in 1983 at AT&T Bell Laboratories, Murray Hill, New Jersey. He is the author of C++ Programming Style (Addison-Wesley, 1992) and welcomes e-mail at [email protected] on Internet and 76476,1422 on CompuServe.


This article is about how multiple inheritance can be used in writing C++ programs. I devote little time to describing the details of language features; these can be found in any C++ text. I concentrate instead on a more subtle issue: identifying the kinds of programming problems for which multiple inheritance really helps programmers.

When multiple inheritance was added to C++ in 1989, I was skeptical that the feature was worth the complexity that it added to the language. Hoping to see its value, I studied all the programs then available that claimed to demonstrate how to use multiple inheritance. However, I discovered that I could rewrite all those programs without using multiple inheritance, and that the resulting programs were generally simpler and easier to understand. My conclusion then was that multiple inheritance in C++ was not useful, and that its complexity was an unnecessary burden for programmers and compiler writers.

More recently, I have seen some programs (and written some myself) that use multiple inheritance in a style quite unlike the earlier ones. I would like to explain why the early attempts to use multiple inheritance failed and what is different about the more recent programs.

I assume only that you know the basic property of multiple inheritance in C++: A derived class (subclass) may inherit from more than one base class (superclass). The programs here do not use any of the tricky parts of multiple inheritance, such as initialization and assignment of virtual base classes or dominance of virtual functions.

Specialization Inheritance

Most inheritance in C++ programs is single inheritance used to express specialization; that is, to model a relationship in which one abstraction is a specialization of another. The more general abstraction is represented by a base class and the specialization is represented by a derived class. Any good text on object-oriented programming in C++ emphasizes specialization as the motivation for using inheritance.

Modeling specialization relationships results in inheritance hierarchies that mirror classification hierarchies from the problem domain. For example, the abstraction of a car is a specialization of the abstraction of a vehicle. Therefore, class Car inherits from class Vehicle. The relationship between class Car and class Vehicle is known as the is-a relationship, or is-a-kind-of relationship, because every Car object is a kind of Vehicle object. The inheritance relationship may be shown graphically in a tree, usually with the base class appearing above the derived class, as in Figure 1 .

Inheritance for Communication

C++ is a statically typed language: The type of every object, pointer, and reference must be declared at compilation time. In order for one object to call a member function (invoke a method) of another object, the calling object must know, at compilation time, the type of the called object. The static type-checking of C++ poses a problem when a server object must perform a callback to a client object. Normally, it is a client object that calls member functions of a server object to obtain services, in which case the client object knows the type of the server object, but not vice versa. A callback occurs when a server object delivers part of its service asynchronously, and must therefore call a member function of the client object.

To see where a callback might arise, consider a simulation that involves engines that may be started by a user clicking with a mouse on an image of a button on a display. The view of an engine object on the screen might be as shown in Figure 2(a) .

Assume that an object of class Engine models the engine, that the Engine object has created a Button object for the Start button, and provided the button object with the information necessary to display itself. The initial communication required is the normal case in which the Engine object is the client and calls member functions of the Button object, the server. In the object diagram shown in Figure 2(b), the arrow represents member-function calls from the Engine object to the Button object.

Such communication does not cause problems for the static type system of C++ because the Engine object knows the class of the server object that it created. The Engine object knows that it is dealing with a Button object and can call the required member functions of Button.

The callback problem arises when the Button object must call back to notify the Engine object that the user has clicked on the buttons image. From the perspective of writing the Engine class, it would be convenient if there were a start member function of Engine, which the Button object could invoke, as in Example 1.

Example 1: Member function in the Engine class that should be "called" back.

  class Engine {
      . . .
  public:
      void start();
      . . .
  };

However, there is no way within the static type system of C++ for the Engine object to tell the Button object, "Please notify me by calling my start member function," unless the Button class knows at compilation time about the Engine class. If Button is a special-purpose class, used only for the Start button of engines, it can be coded with the necessary information about the Engine class. However, it is more useful to create a general-purpose Button server class, one that need know nothing about its clients.

The Button class may be made independent of its clients, if the Button class specifies a callback protocol through an abstract base class. Let there be an auxiliary class, declared alongside Button as part of the Button service, called ButtonCallback, as in Example 2. The entire declaration of the ButtonCallback class is shown. The class defines just one member function, a pure virtual function (deferred method), denoted by the = 0 syntax in the member-function prototype. The purpose of a pure virtual function is to establish an interface; the abstract class declaring a pure virtual function is not obliged to provide an implementation of the function. Classes derived from the abstract class must define the member function. To execute its callback to the client object Button demands that it communicate with an instance of a class derived from ButtonCallback. That derived class must define the callback member function, as shown in the object diagram in Figure 3.

Example 2: Abstract base class that enables callback protocol via pure virtual function callback().

  class ButtonCallback {
  public:
  virtual void   callback() = 0;
  };

As a client receiving the callback, class Engine is declared as a derived class of ButtonCallback and supplies a definition of the callback function. Engine::callback simply calls the start member function of Engine, as shown in Example 3.

Example 3: Engine::callback() in action.

  class Engine : public ButtonCallback
  {
      . . .
  public:
          void start();
  virtual void callback();
      . . .
  };

  void Engine::callback()
  {
      start();
  }

The inheritance relationship between Engine and ButtonCallback is motivated by the need of two objects to communicate in a statically typed language. The inheritance relationship is not motivated by any intrinsic properties of the abstractions involved. There is no attempt to model an is-a-kind-of relationship. Indeed, this use of inheritance is foreign to programmers of dynamically typed languages, such as Smalltalk, where an object sending a message need know nothing at all about the type of the receiving object.

Multiple Inheritance

Multiple inheritance permits a class to be derived from two or more base classes. The simplest multiple-inheritance class diagram has one class, say Z, derived from two others, X and Y, as shown in the class diagram in Figure 4(a).

With this construction permitted in the language, class relationships can become much more involved than with single inheritance. Under single inheritance, the inheritance hierarchy is a tree; under multiple inheritance the hierarchy is a directed acyclic graph, or DAG. For example, an indirect base class can be reached by more than one path through the DAG. The minimal example requires four classes, as shown in Figure 4(b).

Multiple paths through the DAG, may give rise to various kinds of ambiguities, which are addressed by further rules and language mechanisms. Fortunately, we can discuss programming situations in which multiple inheritance is and is not useful without studying these complexities.

Multiple inheritance has caused considerable confusion among programmers and authors of C++ texts. The reaction of most programmers to multiple inheritance is that it must be for expressing relationships among classes that belong to rich classification hierarchies. That is, they look for classes corresponding to abstractions that exhibit more than one is-a-kind-of relationship. The two situations that superficially look most promising are multiple classification and dynamic classification.

Multiple classification arises when an abstraction participates throughout its lifetime in more than one is-a-kind-of relationship. Should the multiple is-a-kind-of relationships be represented by multiple inheritance? Unfortunately, multiple inheritance is an unwieldy way to model multiple classification. On the other hand, multiple classification is simplified by viewing the various attributes of the class independently, and composing those attributes to form an object. Programming with a composition of attributes is generally simpler, more flexible, and more expressive than modeling multiple classification with multiple inheritance.

The following small example is typical of attempts to express multiple classification with multiple inheritance. Suppose that class Car is specialized by engine type, as shown in Figure 5(a). Further suppose that cars are classified by origin of manufacture, as in Figure 5(b). Many programmers try to represent such multiple classification by multiple inheritance within the Car class hierarchy, as in Figure 5(c).

Building programs in this fashion is not viable because of the combinatorial explosion in the number of classes: The number of classes grows as the product of the variation in the attributes. With two attributes the hierarchy is almost manageable, but a real application would quickly grow to hundreds or thousands of classes.

A simpler way to approach this modeling problem is to view the type of the engine and the origin of the car as orthogonal attributes of a car object. The Car class can then declare a member object (or, more precisely, pointer or reference to an object) of the appropriate type for each attribute, as in Example 4.

Example 4: Modeling orthogonal attributes through object reference.

  class Car {
  . . .
  Origin &source;
  Power  &engine;
  . . .
  };

Classes Origin and Power may still form independent inheritance hierarchies, but the Car class is not complicated by specialization relationships among its attribute classes. As further specialization is introduced to these classes, the Car class remains unchanged.

Dynamic classification arises when an abstraction participates in different is-a-kind-of relationships at different phases of its lifetime. For example, sometimes a seaplane is a kind of boat; at other times, it is a kind of plane. Dynamic classification is not supported by any of the inheritance mechanisms of C++, because every object is of precisely one type, determined at the time the object was created. No metamorphosis is permitted; an object cannot modify its type dynamically. If we attempt to express this dynamic classification by multiple inheritance, the class relationship is as shown in Figure 6.

The problem with this class hierarchy is that both class Boat and class Plane are always base classes of SeaPlane. The behavior of a SeaPlane object cannot vary over time; it must always be the union of the behaviors of Boat and Plane.

Dynamic classification can be expressed in C++ by the use of delegation, instead of inheritance. Delegation is relatively simple: One object receives member function calls and propagates them to a member function of another object that performs the delegated work. Using delegation, a SeaPlane object may behave like a boat by delegating incoming calls to a Boat object, as in Figure 7(a). At other times the same SeaPlane object may choose to delegate incoming calls to a Plane object, and therefore behave like a plane, as in Figure 7(b).

Multiple inheritance is not an effective way to program multiple classification or dynamic classification. Quite simply, multiple inheritance in C++ does not support the realization in a computer program of multiple is-a-kind-of relationships.

Multiple Inheritance for Communication

Multiple inheritance does appear to be useful for establishing multiple-communication relationships between objects. Consider a situation in which a single client object must receive callbacks from multiple server objects. An example might be a Clock object that must maintain the time of day in a window on a screen. The Clock object needs the services of two servers: a window server that provides space on the screen in which the image of the clock can be displayed, and an interval timer server. The Clock object must accept callbacks from both server objects: from the Window object, to receive notification that the client must refresh the windows content for some reason, and from the Timer object, to receive notification periodically of the elapsed time, as in Figure 8(a).

By analogy with the inheritance mechanism shown above for communication between Button and Engine, class Clock must be a derived class of both TimerCallback and WindowCallback, as in Figure 8(b).

Notice that this inheritance hierarchy reflects callback-communication relationships between objects. Class Clock is part of an inheritance hierarchy because of the servers from which it requires callbacks, not because of any intrinsic properties of its abstraction.

Synthetic vs. Natural Classes

Class TimerCallback and class WindowCallback are synthetic classes: They do not correspond to abstractions found in an application problem domain. Synthetic classes emerge during design and coding of a system in response to internal, synthetic needs of the software. This is in contrast to natural classes, those that correspond to abstractions from the problem domain and typically arise either during analysis or early in design. A simple criterion for deciding whether a class is natural or synthetic is to ask end users if they recognize the abstraction. Because a natural class comes from the problem domain, an end user will understand its purpose; a synthetic class arises only from software implementation considerations, so the end user will not appreciate the need for it.

Callbacks are not the only reason for creating synthetic classes. They arise anywhere we consider sophisticated implementation details, like object persistence or reference-counted garbage collection.

Conclusion

The better we understand how we use our current programming languages, the better we can guide future languages to meet our needs as programmers. Single inheritance in C++ is useful for dealing with specialization, usually a natural class relationship. Single inheritance is also useful in dealing with synthetic class relationships, such as callbacks. However, multiple inheritance does not appear to help in modeling richer specialization relationships. Composition and delegation turn out to be more useful for that purpose. Multiple inheritance does appear to be useful in managing multiple synthetic class relationships, such as multiple-server callback communication.


Copyright © 1992, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.