Выбрать главу

Classes in Smalltalk usually have a number of things in common, and they always have some things in common (the characteristics and behaviors of Object), so you almost never run into a situation in which 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.

It was not a crystal clear, however, that programmers could not get by without multiple inheritance, and there was (and still is) a lot of disagreement about whether it 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 and exceptions) 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 is seldom involved in your daily design decisions.

One of the most pressing issues at the time 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. 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.

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, that holds graphic images. The only way to make a Holder of BitImages is to derive 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 5, 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 related to design. You can intentionally use MI to make a design more flexible or useful (or at least seemingly so). An example of this is in the original iostream library design (which still persists in today’s template design, as you saw in Chapter 4):

Both istream and ostream are useful classes by themselves, but they can also be derived from simultaneously by a class that combines both their characteristics and behaviors. The class ios provides what is common to all stream classes, and so in this case MI is a code-factoring mechanism.

Regardless of what motivates you to use MI, it’s harder to use than it might appear.

Interface inheritance

One use of multiple inheritance that is not controversial pertains to interface inheritance. In C++, all inheritance is implementation inheritance, because everything in a base class, interface and implementation, becomes part of a derived class. It is not possible to inherit only part of a class (the interface alone, say). As Chapter 14 of Volume 1 explains, private and protected inheritance make it possible to restrict access to members inherited from base classes when used by clients of a derived class object, but this doesn’t affect the derived class; it still contains all base class data and can access all non-private base class members.

Interface inheritance, on the other hand, only adds member function declarations to a derived class interface and is not directly supported in C++. The usual technique to simulate interface inheritance in C++ is to derive from an interface class, which is a class that contains only declarations (no data or function bodies). These declarations will be pure virtual functions, of course. Here is an example.

//: C09:Interfaces.cpp

// Multiple interface inheritance

#include <iostream>

#include <sstream>

#include <string>

using namespace std;

class Printable {

public:

  virtual ~Printable() {}

  virtual void print(ostream&) const = 0;

};

class Intable {

public:

  virtual ~Intable() {}

  virtual int toInt() const = 0;

};

class Stringable {

public:

  virtual ~Stringable() {}

  virtual string toString() const = 0;

};

class Able : public Printable,

             public Intable,

             public Stringable {

  int myData;

public:

  Able(int x) {

    myData = x;

  }

  void print(ostream& os) const {

    os << myData;

  }

  int toInt() const {

    return myData;

  }

  string toString() const {

    ostringstream os;

    os << myData;

    return os.str();

  }

};

void testPrintable(const Printable& p) {

  p.print(cout);

  cout << endl;

}

void testIntable(const Intable& n) {

  int i = n.toInt() + 1;

  cout << i << endl;

}

void testStringable(const Stringable& s) {

  string buf = s.toString() + "th";

  cout << buf << endl;

}

int main() {

  Able a(7);

  testPrintable(a);

  testIntable(a);

  testStringable(a);

} ///:~

Only pure virtual functions are inherited from classes Printable, Intable, and Stringable, which must therefore be implemented in derived class overrides, which the Able class provides. This gives Able objects multiple "is-a" relationships. The object a can act as a Printable object because its class Able derives publicly from Printable and provides an implementation for print( ). The test functions have no need to know the most-derived type of their parameter; they just need an object that is substitutable for their parameter’s type.

As usual, a template solution is more compact:

//: C09:Interfaces2.cpp

// Implicit interface inheritance via templates

#include <iostream>