In This Chapter
Introducing multiple inheritance
Avoiding ambiguities with multiple inheritance
Avoiding ambiguities with virtual inheritance
Figuring out the ordering rules for multiple constructors
Getting a handle on problems with multiple inheritance
In the class hierarchies discussed in other chapters, each class inherits from a single parent. Such single inheritance is sufficient to describe most real-world relationships. Some classes, however, represent the blending of two classes into one. (Sounds sort of romantic, doesn't it?)
An example of such a class is the sleeper sofa that integrates a harsh bed into an uncomfortable sofa. To adequately describe a sleeper sofa in C++, the sleeper sofa should be able to inherit both bed- and sofa-like properties. This is called multiple inheritance.
Figure 25-1 shows the inheritance graph for class SleeperSofa
that inherits both from class Sofa
and from class Bed
.
The code to implement class SleeperSofa
looks like the following:
// // MultipleInheritance - a single class can inherit from // more than one base class // #include <cstdio> #include <cstdlib> #include <iostream> using namespace std; class Bed { public: Bed(){} void sleep(){ cout << "Sleep" << endl; } int weight; }; class Sofa { public: Sofa(){} void watchTV(){ cout << "Watch TV" << endl; } int weight; }; // SleeperSofa - is both a Bed and a Sofa class SleeperSofa : public Bed, public Sofa { public: SleeperSofa(){} void foldOut(){ cout << "Fold out" << endl; }
}; int main(int nNumberofArgs, char* pszArgs[]) { SleeperSofa ss; // you can watch TV on a sleeper sofa like a sofa... ss.watchTV(); // calls Sofa::watchTV() //...and then you can fold it out... ss.foldOut(); // calls SleeperSofa::foldOut() // ...and sleep on it ss.sleep(); // calls Bed::sleep() // wait until user is ready before terminating program // to allow the user to see the program results system("PAUSE"); return 0; }
Here the classes Bed
and Sofa
appear as conventional classes. Unlike in earlier examples, however, the class SleeperSofa
inherits from both Bed
and Sofa
. This is apparent from the appearance of both classes in the class declaration. SleeperSofa
inherits all the members of both base classes. Thus, both of the calls ss.sleep()
and ss.watchTV()
are legal. You can use a SleeperSofa
as a Bed
or a Sofa
. Plus the class SleeperSofa
can have members of its own, such as foldOut()
. The output of this program appears as follows:
Watch TV Fold out Sleep Press any key to continue ...
Is this a great country or what?
Although multiple inheritance is a powerful feature, it introduces several possible problems. One is apparent in the preceding example. Notice that both Bed
and Sofa
contain a member weight
. This is logical because both have a measurable weight. The question is, "Which weight
does SleeperSofa
inherit?"
The answer is "both." SleeperSofa
inherits a member Bed::weight
and a separate member Sofa::weight
. Because they have the same name, unqualified references to weight
are now ambiguous. This is demonstrated in the following snippet, which generates a compile-time error:
#include <iostream> void fn() { SleeperSofa ss; cout << "weight = " << ss.weight // illegal - which weight? << " "; }
The program must now indicate one of the two weights by specifying the desired base class. The following code snippet is correct:
#include <iostream> void fn() { SleeperSofa ss; cout << "sofa weight = " << ss.Sofa::weight // specify which weight << " "; }
Although this solution corrects the problem, specifying the base class in the application function isn't desirable because it forces class information to leak outside the class into application code. In this case, fn()
has to know that SleeperSofa
inherits from Sofa
. These types of so-called name collisions weren't possible with single inheritance but are a constant danger with multiple inheritance.
In the case of SleeperSofa
, the name collision on weight
was more than a mere accident. A SleeperSofa
doesn't have a bed weight separate from its sofa weight. The collision occurred because this class hierarchy doesn't completely describe the real world. Specifically, the classes have not been completely factored.
Thinking about it a little more, it becomes clear that both beds and sofas are special cases of a more fundamental concept: furniture. (I suppose I could get even more fundamental and use something like object with mass, but furniture is fundamental enough.) Weight is a property of all furniture. This relationship is shown in Figure 25-2.
Factoring out the class Furniture
should relieve the name collision. With much relief and great anticipation of success, I generate the C++ class hierarchy shown in the following program, MultipleInheritanceFactoring:
// // MultipleInheritanceFactoring - a single class can // inherit from more than one base class // #include <cstdio> #include <cstdlib> #include <iostream> using namespace std; // Furniture - more fundamental concept; this class // has "weight" as a property class Furniture { public: Furniture(int w) : weight(w) {} int weight; }; class Bed : public Furniture { public:
Bed(int weight) : Furniture(weight) {} void sleep(){ cout << "Sleep" << endl; } }; class Sofa : public Furniture { public: Sofa(int weight) : Furniture(weight) {} void watchTV(){ cout << "Watch TV" << endl; } }; // SleeperSofa - is both a Bed and a Sofa class SleeperSofa : public Bed, public Sofa { public: SleeperSofa(int weight) : Bed(weight), Sofa(weight) {} void foldOut(){ cout << "Fold out" << endl; } }; int main(int nNumberofArgs, char* pszArgs[]) { SleeperSofa ss(10); // Section 1 - // the following is ambiguous; is this a // Furniture::Sofa or a Furniture::Bed? /* cout << "Weight = " << ss.weight << endl; */ // Section 2 - // the following specifies the inheritance path // unambiguously - sort of ruins the effect SleeperSofa* pSS = &ss; Sofa* pSofa = (Sofa*)pSS; Furniture* pFurniture = (Furniture*)pSofa; cout << "Weight = " << pFurniture->weight << endl; // wait until user is ready before terminating program // to allow the user to see the program results system("PAUSE"); return 0; }
Imagine my dismay when I find that this doesn't help at all — the reference to weight
in Section 1 of main()
is still ambiguous. "Okay," I say (not really understanding why weight is still ambiguous), "I'll try casting ss
to a Furniture
."
#include <iostream.h> void fn() { SleeperSofa ss; Furniture* pF; pF = (Furniture*)&ss; // use a Furniture pointer... cout << "weight = " // ...to get at the weight << pF->weight << " "; };
Casting ss
to a Furniture
doesn't work either. Now, I get some strange message that the cast of SleeperSofa*
to Furniture*
is ambiguous. What's going on?
The explanation is straightforward. SleeperSofa
doesn't inherit from Furniture
directly. Both Bed
and Sofa
inherit from Furniture
and then SleeperSofa
inherits from them. In memory, a SleeperSofa
looks like Figure 25-3.
You can see that a SleeperSofa
consists of a complete Bed
followed by a complete Sofa
followed by some SleeperSofa
unique stuff. Each of these subobjects in SleeperSofa
has its own Furniture
part, because each inherits from Furniture
. Thus, a SleeperSofa
contains two Furniture
objects!
I haven't created the hierarchy shown in Figure 25-2 after all. The inheritance hierarchy I've actually created is the one shown in Figure 25-4.
The MultipleInheritanceFactoring program demonstrates this duplication of the base class. Section 2 specifies exactly which weight
object by recasting the pointer SleeperSofa
first to a Sofa*
and then to a Furniture*
.
But SleeperSofa
containing two Furniture
objects is nonsense. SleeperSofa
needs only one copy of Furniture
. I want SleeperSofa
to inherit only one copy of Furniture
, and I want Bed
and Sofa
to share that one copy. C++ calls this virtual inheritance because it uses the virtual keyword.
Armed with this new knowledge, I return to class SleeperSofa
and implement it as follows:
// // VirtualInheritance - using virtual inheritance the // Bed and Sofa classes can share a common base // #include <cstdio> #include <cstdlib> #include <iostream>
using namespace std; // Furniture - more fundamental concept; this class // has "weight" as a property class Furniture { public: Furniture(int w) : weight(w) {} int weight; }; class Bed : virtual public Furniture { public: Bed(int w = 0) : Furniture(w) {} void sleep(){ cout << "Sleep" << endl; } }; class Sofa : virtual public Furniture { public: Sofa(int w = 0) : Furniture(w) {} void watchTV(){ cout << "Watch TV" << endl; } }; // SleeperSofa - is both a Bed and a Sofa class SleeperSofa : public Bed, public Sofa { public: SleeperSofa(int w) : Furniture(w) {} void foldOut(){ cout << "Fold out" << endl; } }; int main(int nNumberofArgs, char* pszArgs[]) { SleeperSofa ss(10); // the following is no longer ambiguous; // there's only one weight shared between Sofa and Bed // Furniture::Sofa or a Furniture::Bed? cout << "Weight = " << ss.weight << endl; // wait until user is ready before terminating program // to allow the user to see the program results system("PAUSE"); return 0; }
Notice the addition of the keyword virtual
in the inheritance of Furniture
in Bed
and Sofa
. This says, "Give me a copy of Furniture
unless you already have one somehow, in which case I'll just use that one." A SleeperSofa
ends up looking like Figure 25-5 in memory.
Here you can see that a SleeperSofa
inherits Furniture
, and then Bed
minus the Furniture
part, followed by Sofa
minus the Furniture
part. Bringing up the rear are the members unique to SleeperSofa
. (Note that this may not be the order of the elements in memory, but that's not important for the purpose of this discussion.)
Now the reference in fn()
to weight
is not ambiguous because a SleeperSofa
contains only one copy of Furniture
. By inheriting Furniture
virtually, you get the desired inheritance relationship as expressed in Figure 25-2.
If virtual inheritance solves this problem so nicely, why isn't it the norm? The first reason is that virtually inherited base classes are handled internally much differently than normally inherited base classes, and these differences involve extra overhead. The second reason is that sometimes you want two copies of the base class.
As an example of the latter, consider a TeacherAssistant
who is both a Student
and a Teacher
, both of which are subclasses of Academician
. If the university gives its teaching assistants two IDs — a student ID and a separate teacher ID — the class TeacherAssistant
will need to contain two copies of class Academician
.
The rules for constructing objects need to be expanded to handle multiple inheritance. The constructors are invoked in the following order:
First, the constructor for any virtual base classes is called in the order in which the classes are inherited.
Then the constructor for all nonvirtual base classes is called in the order in which the classes are inherited.
Next, the constructor for all member objects is called in the order in which the member objects appear in the class.
Finally, the constructor for the class itself is called.
Notice that base classes are constructed in the order in which they are inherited and not in the order in which they appear on the constructor line.
I should point out that not all object-oriented practitioners think that multiple inheritance is a good idea. In addition, many object-oriented languages don't support multiple inheritance.
Multiple inheritance is not an easy thing for the language to implement. This is mostly the compiler's problem (or the compiler writer's problem). But multiple inheritance adds overhead to the code when compared to single inheritance, and this overhead can become the programmer's problem.
More importantly, multiple inheritance opens the door to additional errors. First, ambiguities such as those mentioned in "Straightening Out Inheritance Ambiguities" pop up. Second, in the presence of multiple inheritance, casting a pointer from a subclass to a base class often involves changing the value of the pointer in sophisticated and mysterious ways. Let me leave the details to the language lawyers and compiler writers.
Third, the way in which constructors are invoked can be a little mysterious. Notice in the VirtualInheritance example that SleeperSofa
must invoke the Furniture
constructor directly. The SleeperSofa
cannot initialize weight through either the Bed
or the Sofa
constructors.
I suggest that you avoid using multiple inheritance until you're comfortable with C++. Single inheritance provides enough expressive power to get used to. Later, you can study the manuals until you're sure that you understand exactly what's going on when you use multiple inheritance. One exception is the use of commercial libraries such as Microsoft's Foundation Classes (MFC), which use multiple inheritance quite a bit. These classes have been checked out and are safe.
Don't get me wrong. I'm not out and out against multiple inheritance. The fact that Microsoft and others use multiple inheritance effectively in their class libraries proves that it can be done. However, multiple inheritance is a feature that you want to hold off on using until you're ready for it.
18.118.137.67