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

  cout << "Vendor1::f()\n";

}

Vendor1::~Vendor1() {

  cout << "~Vendor1()\n";

}

void A(const Vendor& V) {

  // ...

  V.v();

  V.f();

  //..

}

void B(const Vendor& V) {

  // ...

  V.v();

  V.f();

  //..

} ///:~

In your project, this source code is unavailable to you. Instead, you get a compiled file as Vendor.obj or Vendor.lib (or with the equivalent file suffixes for your system).

The problem occurs in the use of this library. First, the destructor isn’t virtual. In addition, f( ) was not made virtual; we assume the library creator decided it wouldn’t need to be. And you discover that the interface to the base class is missing a function essential to the solution of your problem. Also suppose you’ve already written a fair amount of code using the existing interface (not to mention the functions A( ) and B( ), which are out of your control), and you don’t want to change it.

To repair the problem, create your own class interface and multiply inherit a new set of derived classes from your interface and from the existing classes:

//: C09:Paste.cpp

//{L} Vendor

// Fixing a mess with MI

#include <iostream>

#include "Vendor.h"

using namespace std;

class MyBase { // Repair Vendor interface

public:

  virtual void v() const = 0;

  virtual void f() const = 0;

  // New interface function:

  virtual void g() const = 0;

  virtual ~MyBase() { cout << "~MyBase()\n"; }

};

class Paste1 : public MyBase, public Vendor1 {

public:

  void v() const {

    cout << "Paste1::v()\n";

    Vendor1::v();

  }

  void f() const {

    cout << "Paste1::f()\n";

    Vendor1::f();

  }

  void g() const {

    cout << "Paste1::g()\n";

  }

  ~Paste1() { cout << "~Paste1()\n"; }

};

int main() {

  Paste1& p1p = *new Paste1;

  MyBase& mp = p1p; // Upcast

  cout << "calling f()\n";

  mp.f();  // Right behavior

  cout << "calling g()\n";

  mp.g(); // New behavior

  cout << "calling A(p1p)\n";

  A(p1p); // Same old behavior

  cout << "calling B(p1p)\n";

  B(p1p);  // Same old behavior

  cout << "delete mp\n";

  // Deleting a reference to a heap object:

  delete &mp; // Right behavior

} ///:~

In MyBase (which does not use MI), both f( ) and the destructor are now virtual, and a new virtual function g( ) is added to the interface. Now each of the derived classes in the original library must be re-created, mixing in the new interface with MI. The functions Paste1::v( ) and Paste1::f( )need to call only the original base-class versions of their functions. But now, if you upcast to MyBase as in main( )

MyBase* mp = p1p; // Upcast

any function calls made through mp will be polymorphic, including delete. Also, the new interface function g( ) can be called through mp. Here’s the output of the program:

calling f()

Paste1::f()

Vendor1::f()

calling g()

Paste1::g()

calling A(p1p)

Paste1::v()

Vendor1::v()

Vendor::f()

calling B(p1p)

Paste1::v()

Vendor1::v()

Vendor::f()

delete mp

~Paste1()

~Vendor1()

~Vendor()

~MyBase()

The original library functions A( ) and B( ) still work the same (assuming the new v( ) calls its base-class version). The destructor is now virtual and exhibits the correct behavior.

Although this is a messy example, it does occur in practice, and it’s a good demonstration of where multiple inheritance is clearly necessary: You must be able to upcast to both base classes.

Summary

One reason MI exists in C++ is that it is a hybrid language and couldn’t enforce a single monolithic class hierarchy the way Smalltalk and Java do. Instead, C++ allows many inheritance trees to be formed, so sometimes you may need to combine the interfaces from two or more trees into a new class.

If no "diamonds" appear in your class hierarchy, MI is fairly simple (although identical function signatures in base classes must still be resolved). If a diamond appears, you may want to eliminate duplicate subobjects by introducing virtual base classes. This not only adds confusion, but the underlying representation becomes more complex and less efficient.

Multiple inheritance has been called the "goto of the ’90s."[115] This seems appropriate because, like a goto, MI is best avoided in normal programming, but can occasionally be very useful. It’s a "minor" but more advanced feature of C++, designed to solve problems that arise in special situations. If you find yourself using it often, you might want to take a look at your reasoning. A good Occam’s Razor is to ask, "Must I upcast to all the base classes?" If not, your life will be easier if you embed instances of all the classes you don’t need to upcast to.

Exercises

These exercises will take you step by step through the complexities of MI.

             1.             Create a base class X with a single constructor that takes an int argument and a member function f( ), which takes no arguments and returns void. Now derive Y and Z from X, creating constructors for each of them that take a single int argument. Now derive A from Y and Z. Create an object of class A, and call f( ) for that object. Fix the problem with explicit disambiguation.

        60.             Starting with the results of exercise 1, create a pointer to an X called px, and assign to it the address of the object of type A you created before. Fix the problem using a virtual base class. Now fix X so you no longer have to call the constructor for X inside A.

         61.             Starting with the results of exercise 2, remove the explicit disambiguation for f( ), and see if you can call f( ) through px. Trace it to see which function gets called. Fix the problem so the correct function will be called in a class hierarchy.

10: Design patterns

"… describe a problem which occurs over and over again in our environment, and then describe the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice" —Christopher Alexander

вернуться

115

A phrase coined by Zack Urlocker.