Listing 14.9 provides a brief test of the classes, using a polymorphic array of pointers.
Listing 14.9. worktest.cpp
// worktest.cpp -- test worker class hierarchy
#include
#include "worker0.h"
const int LIM = 4;
int main()
{
Waiter bob("Bob Apple", 314L, 5);
Singer bev("Beverly Hills", 522L, 3);
Waiter w_temp;
Singer s_temp;
Worker * pw[LIM] = {&bob, &bev, &w_temp, &s_temp};
int i;
for (i = 2; i < LIM; i++)
pw[i]->Set();
for (i = 0; i < LIM; i++)
{
pw[i]->Show();
std::cout << std::endl;
}
return 0;
}
Here is the output of the program in Listings 14.7, 14.8, and 14.9:
Enter waiter's name: Waldo Dropmaster
Enter worker's ID: 442
Enter waiter's panache rating: 3
Enter singer's name: Sylvie Sirenne
Enter worker's ID: 555
Enter number for singer's vocal range:
0: other 1: alto 2: contralto 3: soprano
4: bass 5: baritone 6: tenor
3
Category: waiter
Name: Bob Apple
Employee ID: 314
Panache rating: 5
Category: singer
Name: Beverly Hills
Employee ID: 522
Vocal range: soprano
Category: waiter
Name: Waldo Dropmaster
Employee ID: 442
Panache rating: 3
Category: singer
Name: Sylvie Sirenne
Employee ID: 555
Vocal range: soprano
The design seems to work, with pointers to Waiter invoking Waiter::Show() and Waiter::Set(), and pointers to Singer invoking Singer::Show() and Singer::Set(). However, it leads to some problems if you add a SingingWaiter class derived from both the Singer class and Waiter class. In particular, you’ll need to face the following questions:
• How many workers?
• Which method?
How Many Workers?
Suppose you begin by publicly deriving SingingWaiter from Singer and Waiter:
class SingingWaiter: public Singer, public Waiter {...};
Because both Singer and Waiter inherit a Worker component, SingingWaiter winds up with two Worker components (see Figure 14.4).
Figure 14.4. Inheriting two base-class objects.
As you might expect, this raises problems. For example, ordinarily you can assign the address of a derived-class object to a base-class pointer, but this becomes ambiguous now:
SingingWaiter ed;
Worker * pw = &ed // ambiguous
Normally, such an assignment sets a base-class pointer to the address of the base-class object within the derived object. But ed contains two Worker objects, so there are two addresses from which to choose. You could specify which object by using a type cast:
Worker * pw1 = (Waiter *) &ed // the Worker in Waiter
Worker * pw2 = (Singer *) &ed // the Worker in Singer
This certainly complicates the technique of using an array of base-class pointers to refer to a variety of objects (polymorphism).
Having two copies of a Worker object causes other problems, too. However, the real issue is why should you have two copies of a Worker object at all? A singing waiter, like any other worker, should have just one name and one ID. When C++ added MI to its bag of tricks, it added a virtual base class to make this possible.
Virtual Base Classes
Virtual base classes allow an object derived from multiple bases that themselves share a common base to inherit just one object of that shared base class. For this example, you would make Worker a virtual base class to Singer and Waiter by using the keyword virtual in the class declarations (virtual and public can appear in either order):
class Singer : virtual public Worker {...};
class Waiter : public virtual Worker {...};
Then you would define SingingWaiter as before:
class SingingWaiter: public Singer, public Waiter {...};
Now a SingingWaiter object will contain a single copy of a Worker object. In essence, the inherited Singer and Waiter objects share a common Worker object instead of each bringing in its own copy (see Figure 14.5). Because SingingWaiter now contains one Worker subobject, you can use polymorphism again.
Figure 14.5. Inheritance with a virtual base class.
Let’s look at some questions you might have:
• Why the term
• Why don’t we dispense with declaring base classes virtual and make virtual behavior the norm for MI?
• Are there any catches?
First, why the term virtual? After all, there doesn’t seem to be an obvious connection between the concepts of virtual functions and virtual base classes. There is strong pressure from the C++ community to resist the introduction of new keywords. It would be awkward, for example, if a new keyword corresponded to the name of some important function or variable in a major program. So C++ merely recycled the keyword virtual for the new facility—a bit of keyword overloading.