Inheriting Multiple Inheritance - Security - C++ For Dummies (2014)

C++ For Dummies (2014)

Part V

Security

Chapter 25

Inheriting Multiple Inheritance

In This Chapter

arrow Introducing multiple inheritance

arrow Avoiding ambiguities with multiple inheritance

arrow Avoiding ambiguities with virtual inheritance

arrow Figuring out the ordering rules for multiple constructors

arrow 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 multiple classes into one. (Sounds sort of romantic, doesn't it?)

An example of such a class is the sleeper sofa that creates the unbeatable combination of a harsh bed and 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.

Describing the Multiple Inheritance Mechanism

Figure 25-1 shows the inheritance graph for class SleeperSofa that inherits both from class Sofa and from class Bed.

image

Figure 25-1: Class hierarchy of a sleeper sofa.

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
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
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 Enter to continue...

Is this a great country or what?

Straightening Out Inheritance Ambiguities

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 doesSleeperSofa 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?
<< "\n";
}

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
<< "\n";
}

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.

Adding Virtual 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.

image

Figure 25-2: Further factoring of beds and sofas (by weight).

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>
#define TRYIT false
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?
#if TRYIT
cout << "Weight = " << ss.weight << endl;
#endif

// Section 2 -
// the following specifies the inheritance path
// unambiguously but it 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
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
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
<< "\n";
};

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.

image

Figure 25-3: Memory layout of a SleeperSofa.

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 Furnitureobjects!

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.

image

Figure 25-4: Actual result of my first attempt.

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.

image This is another unfortunate (in my opinion) overloading of a 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
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
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.

image

Figure 25-5: Memory layout of SleeperSofa with virtual inheritance.

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.

Constructing the Objects of Multiple Inheritance

The rules for constructing objects need to be expanded to handle multiple inheritance. The constructors are invoked in the following order:

1. First, the constructor for any virtual base classes is called in the order in which the classes are inherited.

2. Then the constructor for all non-virtual base classes is called in the order in which the classes are inherited.

3. Next, the constructor for all member objects is called in the order in which the member objects appear in the class.

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

Voicing a Contrary Opinion

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 the section “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.

image One exception is that it's fairly safe to multiply inherit a class that contains only pure virtual methods and no data members. This is, in effect, C++'s implementation of what other languages such as Java and C# call an interface. The topic of interfaces is a bit beyond the scope of this book as it's not really a part of C++.