Day 13
Yesterday,
you learned how to write virtual functions in derived classes. This is the
fundamental building block of polymorphism: the capability to bind specific,
derived class objects to base class pointers at runtime. Today, you learn What
multiple inheritance is and how to use it.
What
virtual inheritance is.
What
abstract data types are.
What pure
virtual functions are.
Suppose
you've been working with your animal classes for a while and you've divided the
class hierarchy into Birds and Mammals. The Bird class includes the member
function Fly(). The Mammal class has been divided into a number of types of
Mammals, including Horse. The Horse class includes the member functions
Whinny() and Gallop().
Suddenly,
you realize you need a Pegasus object: a cross between a Horse and a Bird. A
Pegasus can Fly(), it can Whinny(), and it can Gallop(). With single
inheritance, you're in quite a jam.
You can
make Pegasus a Bird, but then it won't be able to Whinny() or Gallop(). You can
make it a Horse, but then it won't be able to Fly().
Your first
solution is to copy the Fly() method into the Pegasus class and derive Pegasus
from Horse. This works fine, at the cost of having the Fly() method in two
places (Bird and Pegasus). If you change one, you must remember to change the
other. Of course, a developer who comes along months or years later to maintain
your code must also know to fix both places.
Soon,
however, you have a new problem. You wish to create a list of Horse objects and
a list of Bird objects. You'd like to be able to add your Pegasus objects to
either list, but if a Pegasus is a horse, you can't add it to a list of birds.
You have a
couple of potential solutions. You can rename the Horse method Gallop() to
Move(), and then override Move() in your Pegasus object to do the work of
Fly(). You would then override Move() in your other horses to do the work of
Gallop(). Perhaps Pegasus could be clever enough to gallop short distances and
fly longer distances.
Pegasus::Move(long
distance)
{
if
(distance > veryFar)
fly(distance);
else
gallop(distance);
}
This is a
bit limiting. Perhaps one day Pegasus will want to fly a short distance or
gallop a long distance. Your next solution might be to move Fly() up into
Horse, as illustrated in Listing 13.1. The problem is that most horses can't
fly, so you have to make this method do nothing unless it is a Pegasus.
View Code
1: // Listing 13.1. If horses could fly...
2: // Percolating Fly() up into Horse
3:
4: #include <iostream.h>
5:
6: class Horse
7: {
8: public:
9: void Gallop(){ cout <<
"Galloping...\n"; }
10: virtual void Fly() { cout <<
"Horses can't fly.\n" ; }
11: private:
12: int itsAge;
13: };
14:
15: class Pegasus : public Horse
16: {
17: public:
18: virtual void Fly() { cout <<
"I can fly! I can fly! I can fly!\n"; }
19: };
20:
21: const int NumberHorses = 5;
22: int main()
23: {
24: Horse* Ranch[NumberHorses];
25: Horse* pHorse;
26: int choice,i;
27: for (i=0; i<NumberHorses; i++)
28: {
29: cout << "(1)Horse
(2)Pegasus: ";
30: cin >> choice;
31: if (choice == 2)
32: pHorse = new Pegasus;
33: else
34: pHorse = new Horse;
35: Ranch[i] = pHorse;
36: }
37: cout << "\n";
38: for (i=0; i<NumberHorses; i++)
39: {
40: Ranch[i]->Fly();
41: delete Ranch[i];
42: }
43: return 0;
44: }
Output:
(1)Horse (2)Pegasus: 1
(1)Horse
(2)Pegasus: 2
(1)Horse
(2)Pegasus: 1
(1)Horse
(2)Pegasus: 2
(1)Horse
(2)Pegasus: 1
Horses
can't fly.
I can
fly! I can fly! I can fly!
Horses
can't fly.
I can
fly! I can fly! I can fly!
Horses can't fly.
Analysis:
This program certainly works, though at the expense of the Horse class having a
Fly() method. On line 10, the method Fly() is provided to Horse. In a
real-world class, you might have it issue an error, or fail quietly. On line
18, the Pegasus class overrides the Fly() method to "do the right
thing," represented here by printing a happy message.
The array
of Horse pointers on line 24 is used to demonstrate that the correct Fly()
method is called based on the runtime binding of the Horse or Pegasus object.
Putting the
required function higher in the class hierarchy is a common solution to this
problem and results in many functions "percolating up" into the base
class. The base class is then in grave danger of becoming a global namespace
for all the functions that might be used by any of the derived classes. This
can seriously undermine the class typing of C++, and can create a large and
cumbersome base class.
In general,
you want to percolate shared functionality up the hierarchy, without migrating
the interface of each class. This means that if two classes that share a common
base class (for example, Horse and Bird both share Animal) have a function in
common (both birds and horses eat, for example), you'll want to move that
functionality up into the base class and create a virtual function.
What you'll
want to avoid, however, is percolating an interface (like Fly up where it
doesn't belong), just so you can call that function only on some derived
classes.
An
alternative to this approach, still within single inheritance, is to keep the
Fly() method within Pegasus, and only call it if the pointer is actually
pointing to a Pegasus object. To make this work, you'll need to be able to ask
your pointer what type it is really pointing to. This is known as Run Time Type
Identification (RTTI). Using RTTI has only recently become an official part of
C++.
If your
compiler does not support RTTI, you can mimic it by putting a method that returns
an enumerated type in each of the classes. You can then test that type at
runtime and call Fly() if it returns Pegasus.
--------------------------------------------------------------------------------
NOTE:
Beware of adding RTTI to your classes. Use of it may be an indication of poor
design. Consider using virtual functions, templates, or multiple inheritance
instead.
--------------------------------------------------------------------------------
In order to
call Fly() however, you must cast the pointer, telling it that the object it is
pointing to is a Pegasus object, not a Horse. This is called casting down,
because you are casting the Horse object down to a more derived type.
C++ now
officially, though perhaps reluctantly, supports casting down using the new
dynamic_cast operator. Here's how it works.
If you have
a pointer to a base class such as Horse, and you assign to it a pointer to a
derived class, such as Pegasus, you can use the Horse pointer polymorphically.
If you then need to get at the Pegasus object, you create a Pegasus pointer and
use the dynamic_cast operator to make the conversion.
At runtime,
the base pointer will be examined. If the conversion is proper, your new
Pegasus pointer will be fine. If the conversion is improper, if you didn't
really have a Pegasus object after all, then your new pointer will be null.
Listing 13.2 illustrates this point.
1: // Listing 13.2 Using dynamic_cast.
2: // Using rtti
3:
4: #include <iostream.h>
5: enum TYPE { HORSE, PEGASUS };
6:
7: class Horse
8: {
9: public:
10: virtual void Gallop(){ cout <<
"Galloping...\n"; }
11:
12: private:
13: int itsAge;
14: };
15:
16: class Pegasus : public Horse
17: {
18: public:
19:
20: virtual void Fly() { cout <<
"I can fly! I can fly! I can fly!\n"; }
21: };
22:
23: const int NumberHorses = 5;
24: int main()
25: {
26: Horse* Ranch[NumberHorses];
27: Horse* pHorse;
28: int choice,i;
29: for (i=0; i<NumberHorses; i++)
30: {
31: cout << "(1)Horse
(2)Pegasus: ";
32: cin >> choice;
33: if (choice == 2)
34: pHorse = new Pegasus;
35: else
36: pHorse = new Horse;
37: Ranch[i] = pHorse;
38: }
39: cout << "\n";
40: for (i=0; i<NumberHorses; i++)
41: {
42: Pegasus *pPeg = dynamic_cast<
Pegasus *> (Ranch[i]);
42: if (pPeg)
43: pPeg->Fly();
44: else
45: cout << "Just a
horse\n";
46:
47: delete Ranch[i];
48: }
49: return 0;
50:
Output:
(1)Horse (2)Pegasus: 1
(1)Horse
(2)Pegasus: 2
(1)Horse
(2)Pegasus: 1
(1)Horse
(2)Pegasus: 2
(1)Horse
(2)Pegasus: 1
Just a
horse
I can fly!
I can fly! I can fly!
Just a
horse
I can
fly! I can fly! I can fly!
Just a
horse
Analysis:
This solution also works. Fly() is kept out of Horse, and is not called on
Horse objects. When it is called on Pegasus objects, however, they must be
explicitly cast; Horse objects don't have the method Fly(), so the pointer must
be told it is pointing to a Pegasus object before being used.
The need
for you to cast the Pegasus object is a warning that something may be wrong
with your design. This program effectively undermines the virtual function
polymorphism, because it depends on casting the object to its real runtime
type.
The other
problem with these solutions is that you've declared Pegasus to be a type of
Horse, so you cannot add a Pegasus object to a list of Birds. You've paid the
price of either moving Fly() up into Horse, or casting down the pointer, and
yet you still don't have the full functionality you need.
One final
single inheritance solution presents itself. You can push Fly(), Whinny(), and
Gallop() all up into a common base class of both Bird and Horse: Animal. Now,
instead of having a list of Birds and a list of Horses, you can have one
unified list of Animals. This works, but percolates more functionality up into
the base classes.
Alternatively,
you can leave the methods where they are, but cast down Horses and Birds and
Pegasus objects, but that is even worse!
--------------------------------------------------------------------------------
DO move
functionality up the inheritance hierarchy. DON'T move interface up the
inheritance hierarchy. DO avoid switching on the runtime type of the
object--use virtual methods, templates, and multiple inheritance. DON'T cast
pointers to base objects down to derived objects.
--------------------------------------------------------------------------------
Multiple
Inheritance
It is
possible to derive a new class from more than one base class. This is called
Multiple Inheritance. To derive from more than the base class, you separate
each base class by commas in the class designation. Listing 13.3 illustrates
how to declare Pegasus so that it derives from both Horses and Birds. The
program then adds Pegasus objects to both types of lists.
1: // Listing 13.3. Multiple inheritance.
2: // Multiple Inheritance
3:
4: #include <iostream.h>
5:
6: class Horse
7: {
8: public:
9: Horse() { cout << "Horse
constructor... "; }
10: virtual ~Horse() { cout <<
"Horse destructor... "; }
11: virtual void Whinny() const { cout
<< "Whinny!... "; }
12: private:
13: int itsAge;
14: };
15:
16: class Bird
17: {
18: public:
19: Bird() { cout << "Bird
constructor... "; }
20: virtual ~Bird() { cout <<
"Bird destructor... "; }
21: virtual void Chirp() const { cout
<< "Chirp... "; }
22: virtual void Fly() const
23: {
24: cout << "I can fly! I can
fly! I can fly! ";
25: }
26: private:
27: int itsWeight;
28: };
29:
30: class Pegasus : public Horse, public Bird
31: {
32: public:
33: void Chirp() const { Whinny(); }
34: Pegasus() { cout << "Pegasus
constructor... "; }
35: ~Pegasus() { cout << "Pegasus
destructor... "; }
36: };
37:
38: const int MagicNumber = 2;
39: int main()
40: {
41: Horse* Ranch[MagicNumber];
42: Bird* Aviary[MagicNumber];
43: Horse * pHorse;
44: Bird * pBird;
45: int choice,i;
46: for (i=0; i<MagicNumber; i++)
47: {
48: cout << "\n(1)Horse
(2)Pegasus: ";
49: cin >> choice;
50: if (choice == 2)
51: pHorse = new Pegasus;
52: else
53: pHorse = new Horse;
54: Ranch[i] = pHorse;
55: }
56: for (i=0; i<MagicNumber; i++)
57: {
58: cout << "\n(1)Bird
(2)Pegasus: ";
59: cin >> choice;
60: if (choice == 2)
61: pBird = new Pegasus;
62: else
63: pBird = new Bird;
64: Aviary[i] = pBird;
65: }
66:
67: cout << "\n";
68: for (i=0; i<MagicNumber; i++)
69: {
70: cout << "\nRanch["
<< i << "]: " ;
71: Ranch[i]->Whinny();
72: delete Ranch[i];
73: }
74:
75: for (i=0; i<MagicNumber; i++)
76: {
77: cout << "\nAviary["
<< i << "]: " ;
78: Aviary[i]->Chirp();
79: Aviary[i]->Fly();
80: delete Aviary[i];
81: }
82: return 0;
83: }
Output:
(1)Horse (2)Pegasus: 1
Horse constructor...
(1)Horse
(2)Pegasus: 2
Horse
constructor... Bird constructor... Pegasus constructor...
(1)Bird
(2)Pegasus: 1
Bird
constructor...
(1)Bird
(2)Pegasus: 2
Horse
constructor... Bird constructor... Pegasus constructor...
Ranch[0]:
Whinny!... Horse destructor...
Ranch[1]:
Whinny!... Pegasus
destructor... Bird destructor... Horse
destructor...
Aviary[0]:
Chirp... I can fly! I can fly! I can fly! Bird destructor...
Aviary[1]:
Whinny!... I can fly! I can fly! I can fly!
Pegasus
destructor... Bird destructor... Horse destructor...
Aviary[0]:
Chirp... I can fly!
I can
fly! I can fly! Bird destructor...
Aviary[1]:
Whinny!... I can fly! I can fly! I can fly!
Pegasus
destructor.. Bird destructor... Horse destructor...
Analysis:
On lines 6-14, a Horse class is declared. The constructor and destructor print
out a message, and the Whinny() method prints the word Whinny!
On lines
16-25, a Bird class is declared. In addition to its constructor and destructor,
this class has two methods: Chirp() and Fly(), both of which print identifying
messages. In a real program these might, for example, activate the speaker or
generate animated images.
Finally, on
lines 30-36, the class Pegasus is declared. It derives both from Horse and from
Bird. The Pegasus class overrides the Chirp() method to call the Whinny()
method, which it inherits from Horse.
Two lists
are created, a Ranch with pointers to Horse on line 41, and an Aviary with
pointers to Bird on line 42. On lines 46-55, Horse and Pegasus objects are
added to the Ranch. On lines 56-65, Bird and Pegasus objects are added to the
Aviary.
Invocations
of the virtual methods on both the Bird pointers and the Horse pointers do the
right things for Pegasus objects. For example, on line 78 the members of the
Aviary array are used to call Chirp() on the objects to which they point. The
Bird class declares this to be a virtual method, so the right function is
called for each object.
Note that
each time a Pegasus object is created, the output reflects that both the Bird
part and the Horse part of the Pegasus object is also created. When a Pegasus
object is destroyed, the Bird and Horse parts are destroyed as well, thanks to
the destructors being made virtual.
Declaring Multiple Inheritance
Declare an
object to inherit from more than one class by listing the base classes
following the colon after the class name. Separate the base classes by commas.
Example 1:
class
Pegasus : public Horse, public Bird
Example 2:
class
Schnoodle : public Schnauzer, public Poodle
The Parts
of a Multiply Inherited Object
When the
Pegasus object is created in memory, both of the base classes form part of the
Pegasus object, as illustrated in Figure 13.1.
A number of issues arise with objects with
multiple base classes. For example, what happens if two base classes that
happen to have the same name have virtual functions or data? How are multiple
base class constructors initialized? What happens if multiple base classes both
derive from the same class? The next sections will answer these questions, and
explore how multiple inheritance can be put to work.
Constructors in Multiply Inherited Objects
If Pegasus
derives from both Horse and Bird, and each of the base classes has constructors
that take parameters, the Pegasus class initializes these constructors in turn.
Listing 13.4 illustrates how this is done.
1: // Listing 13.4
2: // Calling multiple constructors
3: #include <iostream.h>
4: typedef int HANDS;
5: enum COLOR { Red, Green, Blue, Yellow,
White, Black, Brown } ;
6: enum BOOL { FALSE, TRUE };
7:
8: class Horse
9: {
10: public:
11: Horse(COLOR color, HANDS height);
12: virtual ~Horse() { cout <<
"Horse destructor...\n"; }
13: virtual void Whinny()const { cout
<< "Whinny!... "; }
14: virtual HANDS GetHeight() const { return
itsHeight; }
15: virtual COLOR GetColor() const { return
itsColor; }
16: private:
17: HANDS itsHeight;
18: COLOR itsColor;
19: };
20:
21: Horse::Horse(COLOR color, HANDS height):
22: itsColor(color),itsHeight(height)
23: {
24: cout << "Horse
constructor...\n";
25: }
26:
27: class Bird
28: {
29: public:
30: Bird(COLOR color, BOOL migrates);
31: virtual ~Bird() {cout <<
"Bird destructor...\n"; }
32: virtual void Chirp()const { cout
<< "Chirp... "; }
33: virtual void Fly()const
34: {
35: cout << "I can fly! I can
fly! I can fly! ";
36: }
37: virtual COLOR GetColor()const { return
itsColor; }
38: virtual BOOL GetMigration() const {
return itsMigration; }
39:
40: private:
41: COLOR itsColor;
42: BOOL itsMigration;
43: };
44:
45: Bird::Bird(COLOR color, BOOL migrates):
46: itsColor(color), itsMigration(migrates)
47: {
48: cout << "Bird
constructor...\n";
49: }
50:
51: class Pegasus : public Horse, public Bird
52: {
53: public:
54: void Chirp()const { Whinny(); }
55: Pegasus(COLOR, HANDS, BOOL,long);
56: ~Pegasus() {cout << "Pegasus
destructor...\n";}
57: virtual long GetNumberBelievers() const
58: {
59: return itsNumberBelievers;
60: }
61:
62: private:
63: long itsNumberBelievers;
64: };
65:
66: Pegasus::Pegasus(
67: COLOR aColor,
68: HANDS height,
69: BOOL migrates,
70: long NumBelieve):
71: Horse(aColor, height),
72: Bird(aColor, migrates),
73: itsNumberBelievers(NumBelieve)
74: {
75: cout << "Pegasus
constructor...\n";
76: }
77:
78: int main()
79: {
80: Pegasus *pPeg = new Pegasus(Red, 5,
TRUE, 10);
81: pPeg->Fly();
82: pPeg->Whinny();
83: cout << "\nYour Pegasus is
" << pPeg->GetHeight();
84: cout << " hands tall and
";
85: if (pPeg->GetMigration())
86: cout << "it does
migrate.";
87: else
88: cout << "it does not
migrate.";
89: cout << "\nA total of "
<< pPeg->GetNumberBelievers();
90: cout << " people believe it exists.\n";
91: delete pPeg;
92: return 0;
93: }
Output:
Horse constructor...
Bird constructor...
Pegasus constructor...
I can
fly! I can fly! I can fly! Whinny!...
Your
Pegasus is 5 hands tall and it does migrate.
A total
of 10 people believe it exists.
Pegasus
destructor...
Bird
destructor...
Horse
destructor...
Analysis:
On lines 8-19, the Horse class is declared. The constructor takes two
parameters, both using enumerations declared on lines 5 and 6. The
implementation of the constructor on lines 21-25 simply initializes the member
variables and prints a message.
On lines
27-43, the Bird class is declared, and the implementation of its constructor is
on lines 45-49. Again, the Bird class takes two parameters. Interestingly, the
Horse constructor takes color (so that you can detect horses of different
colors), and the Bird constructor takes the color of the feathers (so those of
one feather can stick together). This leads to a problem when you want to ask
the Pegasus for its color, which you'll see in the next example.
The Pegasus
class itself is declared on lines 51-64, and its constructor is on lines 66-72.
The initialization of the Pegasus object includes three statements. First, the
Horse constructor is initialized with color and height. Then the Bird
constructor is initialized with color and the Boolean. Finally, the Pegasus
member variable itsNumberBelievers is initialized. Once all that is
accomplished, the body of the Pegasus constructor is called.
In the
main() function, a Pegasus pointer is created and used to access the member
functions of the base objects.
Ambiguity Resolution
In Listing
13.4, both the Horse class and the Bird class have a method GetColor(). You may
need to ask the Pegasus object to return its color, but you have a problem: the
Pegasus class inherits from both Bird and Horse. They both have a color, and
their methods for getting that color have the same names and signature. This
creates an ambiguity for the compiler, which you must resolve.
If you
simply write
COLOR
currentColor = pPeg->GetColor();
you will
get a compiler error:
Member is
ambiguous: `Horse::GetColor' and `Bird::GetColor'
You can
resolve the ambiguity with an explicit call to the function you wish to invoke:
COLOR
currentColor = pPeg->Horse::GetColor();
Anytime you
need to resolve which class a member function or member data inherits from, you
can fully qualify the call by prepending the class name to the base class data
or function.
Note that
if Pegasus were to override this function, the problem would be moved, as it
should be, into the Pegasus member function:
virtual
COLOR GetColor()const { return Horse::itsColor; }
This hides
the problem from clients of the Pegasus class, and encapsulates within Pegasus
the knowledge of which base class it wishes to inherit its color from. A client
is still free to force the issue by writing:
COLOR
currentColor = pPeg->Bird::GetColor();
What
happens if both Bird and Horse inherit from a common base class, such as Animal?
Figure 13.2 illustrates what this looks like.
As you can
see in Figure 13.2, two base class objects exist. When a function or data
member is called in the shared base class, another ambiguity exists. For
example, if Animal declares itsAge as a member variable and GetAge() as a
member function, and you call pPeg->GetAge(), did you mean to call the
GetAge() function you inherit from Animal by way of Horse, or by way of Bird?
You must resolve this ambiguity as well, as illustrated in Listing 13.5.
1: // Listing 13.5
2: // Common base classes
3: #include <iostream.h>
4:
5: typedef int HANDS;
6: enum COLOR { Red, Green, Blue, Yellow,
White, Black, Brown } ;
7: enum BOOL { FALSE, TRUE };
8:
9: class Animal // common base to both horse and bird
10: {
11: public:
12: Animal(int);
13: virtual ~Animal() { cout <<
"Animal destructor...\n"; }
14: virtual int GetAge() const { return
itsAge; }
15: virtual void SetAge(int age) { itsAge =
age; }
16: private:
17: int itsAge;
18: };
19:
20: Animal::Animal(int age):
21: itsAge(age)
22: {
23: cout << "Animal
constructor...\n";
24: }
25:
26: class Horse : public Animal
27: {
28: public:
29: Horse(COLOR color, HANDS height, int
age);
30: virtual ~Horse() { cout <<
"Horse destructor...\n"; }
31: virtual void Whinny()const { cout
<< "Whinny!... "; }
32: virtual HANDS GetHeight() const { return
itsHeight; }
33: virtual COLOR GetColor() const { return
itsColor; }
34: protected:
35: HANDS itsHeight;
36: COLOR itsColor;
37: };
38:
39: Horse::Horse(COLOR color, HANDS height, int
age):
40: Animal(age),
41: itsColor(color),itsHeight(height)
42: {
43: cout << "Horse
constructor...\n";
44: }
45:
46: class Bird : public Animal
47: {
48: public:
49: Bird(COLOR color, BOOL migrates, int
age);
50: virtual ~Bird() {cout <<
"Bird destructor...\n"; }
51: virtual void Chirp()const { cout
<< "Chirp... "; }
52: virtual void Fly()const
53: { cout << "I can fly! I
can fly! I can fly! "; }
54: virtual COLOR GetColor()const { return
itsColor; }
55: virtual BOOL GetMigration() const {
return itsMigration; }
56: protected:
57: COLOR itsColor;
58: BOOL itsMigration;
59: };
60:
61: Bird::Bird(COLOR color, BOOL migrates, int
age):
62: Animal(age),
63: itsColor(color), itsMigration(migrates)
64: {
65: cout << "Bird
constructor...\n";
66: }
67:
68: class Pegasus : public Horse, public Bird
69: {
70: public:
71: void Chirp()const { Whinny(); }
72: Pegasus(COLOR, HANDS, BOOL, long, int);
73: ~Pegasus() {cout << "Pegasus
destructor...\n";}
74: virtual long GetNumberBelievers() const
75: { return itsNumberBelievers; }
76: virtual COLOR GetColor()const { return
Horse::itsColor; }
77: virtual int GetAge() const { return
Horse::GetAge(); }
78: private:
79: long itsNumberBelievers;
80: };
81:
82: Pegasus::Pegasus(
83: COLOR aColor,
84: HANDS height,
85: BOOL migrates,
86: long NumBelieve,
87: int age):
88: Horse(aColor, height,age),
89: Bird(aColor, migrates,age),
90: itsNumberBelievers(NumBelieve)
91: {
92: cout << "Pegasus
constructor...\n";
93: }
94:
95: int main()
96: {
97: Pegasus *pPeg = new Pegasus(Red, 5,
TRUE, 10, 2);
98: int age = pPeg->GetAge();
99: cout << "This pegasus is
" << age << " years old.\n";
100: delete pPeg;
101: return 0;
102: }
Output:
Animal constructor...
Horse constructor...
Animal
constructor...
Bird
constructor...
Pegasus
constructor...
This
pegasus is 2 years old.
Pegasus destructor...
Bird
destructor...
Animal
destructor...
Horse
destructor...
Animal
destructor...
Analysis:
There are a number of interesting features to this listing. The Animal class is
declared on lines 9-18. Animal adds one member variable, itsAge and an accessor,
SetAge().
On line 26,
the Horse class is declared to derive from Animal. The Horse constructor now
has a third parameter, age, which it passes to its base class, Animal. Note
that the Horse class does not override GetAge(), it simply inherits it.
On line 46,
the Bird class is declared to derive from Animal. Its constructor also takes an
age and uses it to initialize its base class, Animal. It also inherits GetAge()
without overriding it.
Pegasus
inherits from both Bird and from Animal, and so has two Animal classes in its
inheritance chain. If you were to call GetAge() on a Pegasus object, you would
have to disambiguate, or fully qualify, the method you want if Pegasus did not
override the method.
This is
solved on line 76 when the Pegasus object overrides GetAge() to do nothing more
than to chain up--that is, to call the same method in a base class.
Chaining up
is done for two reasons: either to disambiguate which base class to call, as in
this case, or to do some work and then let the function in the base class do
some more work. At times, you may want to do work and then chain up, or chain
up and then do the work when the base class function returns.
The Pegasus
constructor takes five parameters: the creature's color, its height (in HANDS),
whether or not it migrates, how many believe in it, and its age. The
constructor initializes the Horse part of the Pegasus with the color, height,
and age on line 88. It initializes the Bird part with color, whether it
migrates, and age on line 89. Finally, it initializes itsNumberBelievers on
line 90.
The call to
the Horse constructor on line 88 invokes the implementation shown on line 39.
The Horse constructor uses the age parameter to initialize the Animal part of
the Horse part of the Pegasus. It then goes on to initialize the two member
variables of Horse--itsColor and itsAge.
The call to
the Bird constructor on line 89 invokes the implementation shown on line 46.
Here too, the age parameter is used to initialize the Animal part of the Bird.
Note that
the color parameter to the Pegasus is used to initialize member variables in
each of Bird and Horse. Note also that the age is used to initialize itsAge in
the Horse's base Animal and in the Bird's base Animal.
In Listing
13.5, the Pegasus class went to some lengths to disambiguate which of its
Animal base classes it meant to invoke. Most of the time, the decision as to
which one to use is arbitrary--after all, the Horse and the Bird have exactly
the same base class.
It is
possible to tell C++ that you do not want two copies of the shared base class,
as shown in Figure 13.2, but rather to have a single shared base class, as
shown in Figure 13.3.
You
accomplish this by making Animal a virtual base class of both Horse and Bird.
The Animal class does not change at all. The Horse and Bird classes change only
in their use of the term virtual in their declarations. Pegasus, however,
changes substantially.
Normally, a
class's constructor initializes only its own variables and its base class.
Virtually inherited base classes are an exception, however. They are
initialized by their most derived class. Thus, Animal is initialized not by
Horse and Bird, but by Pegasus. Horse and Bird have to initialize Animal in
their constructors, but these initializations will be ignored when a Pegasus
object is created.
1: // Listing 13.6
2: // Virtual inheritance
3: #include <iostream.h>
4:
5: typedef int HANDS;
6: enum COLOR { Red, Green, Blue, Yellow,
White, Black, Brown } ;
7: enum BOOL { FALSE, TRUE };
8:
9: class Animal // common base to both horse and bird
10: {
11: public:
12: Animal(int);
13: virtual ~Animal() { cout <<
"Animal destructor...\n"; }
14: virtual int GetAge() const { return
itsAge; }
15: virtual void SetAge(int age) { itsAge =
age; }
16: private:
17: int itsAge;
18: };
19:
20: Animal::Animal(int age):
21: itsAge(age)
22: {
23: cout << "Animal
constructor...\n";
24: }
25:
26: class Horse : virtual public Animal
27: {
28: public:
29: Horse(COLOR color, HANDS height, int
age);
30: virtual ~Horse() { cout <<
"Horse destructor...\n"; }
31: virtual void Whinny()const { cout
<< "Whinny!... "; }
32: virtual HANDS GetHeight() const { return
itsHeight; }
33: virtual COLOR GetColor() const { return
itsColor; }
34: protected:
35: HANDS itsHeight;
36: COLOR itsColor;
37: };
38:
39: Horse::Horse(COLOR color, HANDS height, int
age):
40: Animal(age),
41: itsColor(color),itsHeight(height)
42: {
43: cout << "Horse
constructor...\n";
44: }
45:
46: class Bird : virtual public Animal
47: {
48: public:
49: Bird(COLOR color, BOOL migrates, int
age);
50: virtual ~Bird() {cout <<
"Bird destructor...\n"; }
51: virtual void Chirp()const { cout
<< "Chirp... "; }
52: virtual void Fly()const
53: { cout << "I can fly! I
can fly! I can fly! "; }
54: virtual COLOR GetColor()const { return
itsColor; }
55: virtual BOOL GetMigration() const {
return itsMigration; }
56: protected:
57: COLOR itsColor;
58: BOOL itsMigration;
59: };
60:
61: Bird::Bird(COLOR color, BOOL migrates, int
age):
62: Animal(age),
63: itsColor(color), itsMigration(migrates)
64: {
65: cout << "Bird
constructor...\n";
66: }
67:
68: class Pegasus : public Horse, public Bird
69: {
70: public:
71: void Chirp()const { Whinny(); }
72: Pegasus(COLOR, HANDS, BOOL, long, int);
73: ~Pegasus() {cout << "Pegasus
destructor...\n";}
74: virtual long GetNumberBelievers() const
75: { return itsNumberBelievers; }
76: virtual COLOR GetColor()const { return
Horse::itsColor; }
77: private:
78: long itsNumberBelievers;
79: };
80:
81: Pegasus::Pegasus(
82: COLOR aColor,
83: HANDS height,
84: BOOL migrates,
85: long NumBelieve,
86: int age):
87: Horse(aColor, height,age),
88: Bird(aColor, migrates,age),
89: Animal(age*2),
90: itsNumberBelievers(NumBelieve)
91: {
92: cout << "Pegasus
constructor...\n";
93: }
94:
95: int main()
96: {
97: Pegasus *pPeg = new Pegasus(Red, 5,
TRUE, 10, 2);
98: int age = pPeg->GetAge();
99: cout << "This pegasus is
" << age << " years old.\n";
100: delete pPeg;
101: return 0;
102: }
Output: Animal
constructor...
Horse
constructor...
Bird
constructor...
Pegasus
constructor...
This
pegasus is 4 years old.
Pegasus destructor...
Bird destructor...
Horse destructor...
Animal destructor...
Analysis:
On line 26, Horse declares that it inherits virtually from Animal, and on line
46, Bird makes the same declaration. Note that the constructors for both Bird
and Animal still initialize the Animal object.
Pegasus
inherits from both Bird and Animal, and as the most derived object of Animal,
it also initializes Animal. It is Pegasus' initialization which is called,
however, and the calls to Animal's constructor in Bird and Horse are ignored.
You can see this because the value 2 is passed in, and Horse and Bird pass it
along to Animal, but Pegasus doubles it. The result, 4, is reflected in the
printout on line 99 and as shown in the output.
Pegasus no
longer has to disambiguate the call to GetAge(), and so is free to simply
inherit this function from Animal. Note that Pegasus must still disambiguate
the call to GetColor(), as this function is in both of its base classes and not
in Animal.
To ensure
that derived classes have only one instance of common base classes, declare the
intermediate classes to inherit virtually from the base class. Example 1:
class Horse
: virtual public Animal
class Bird
: virtual public Animal
class
Pegasus : public Horse, public Bird
Example 2:
class
Schnauzer : virtual public Dog
class
Poodle : virtual public Dog
class
Schnoodle : public Schnauzer, public Poodle
Problems
with Multiple Inheritance
Although
multiple inheritance offers a number of advantages over single inheritance,
there are many C++ programmers who are reluctant to use it. The problems they
cite are that many compilers don't support it yet, that it makes debugging
harder, and that nearly everything that can be done with multiple inheritance
can be done without it.
These are
valid concerns, and you will want to be on your guard against installing
needless complexity into your programs. Some debuggers have a hard time with
multiple inheritance, and some designs are needlessly made complex by using
multiple inheritance when it is not needed.
--------------------------------------------------------------------------------
DO use
multiple inheritance when a new class needs functions and features from more
than one base class. DO use virtual inheritance when the most derived classes
must have only one instance of the shared base class. DO initialize the shared
base class from the most derived class when using virtual base classes. DON'T
use multiple inheritance when single inheritance will do.
--------------------------------------------------------------------------------
One way to
strike a middle ground between multiple inheritance and single inheritance is
to use what are called mixins. Thus, you might have your Horse class derive
from Animal and from Displayable. Displayable would just add a few methods for
displaying any object onscreen.
--------------------------------------------------------------------------------
New Term: A
mixin , or capability class, is a class that adds functionality without adding
much or any data.
--------------------------------------------------------------------------------
Capability
classes are mixed into a derived class like any other class might be, by
declaring the derived class to inherit publicly from them. The only difference
between a capability class and any other class is that the capability class has
little or no data. This is an arbitrary distinction, of course, and is just a
shorthand way of noting that at times all you want to do is mix in some
additional capabilities without complicating the derived class.
This will,
for some debuggers, make it easier to work with mixins than with more complex
multiply inherited objects. There is also less likelihood of ambiguity in
accessing the data in the other principal base class.
For
example, if Horse derives from Animal and from Displayable, Displayable would
have no data. Animal would be just as it always was, so all the data in Horse
would derive from Animal, but the functions in Horse would derive from both.
The term
mixin comes from an ice-cream store in
Often, you
will create a hierarchy of classes together. For example, you might create a
Shape class, and derive from that Rectangle and Circle. From Rectangle, you
might derive Square, as a special case of Rectangle.
Each of the
derived classes will override the Draw() method, the GetArea() method, and so
forth. Listing 13.7 illustrates a bare-bones implementation of the Shape class
and its derived Circle and Rectangle classes.
1: //Listing 13.7. Shape classes.
2:
3: #include <iostream.h>
4:
5: enum BOOL { FALSE, TRUE };
6:
7: class Shape
8: {
9: public:
10: Shape(){}
11: ~Shape(){}
12: virtual long GetArea() { return -1; } //
error
13: virtual long GetPerim() { return -1; }
14: virtual void Draw() {}
15: private:
16: };
17:
18: class Circle : public Shape
19: {
20: public:
21: Circle(int
radius):itsRadius(radius){}
22: ~Circle(){}
23: long GetArea() { return 3 * itsRadius
* itsRadius; }
24: long GetPerim() { return 9 *
itsRadius; }
25: void Draw();
26: private:
27: int itsRadius;
28: int itsCircumference;
29: };
30:
31: void Circle::Draw()
32: {
33: cout << "Circle drawing
routine here!\n";
34: }
35:
36:
37: class Rectangle : public Shape
38: {
39: public:
40: Rectangle(int len, int width):
41: itsLength(len), itsWidth(width){}
42: ~Rectangle(){}
43: virtual long GetArea() { return
itsLength * itsWidth; }
44: virtual long GetPerim() {return
2*itsLength + 2*itsWidth; }
45: virtual int GetLength() { return
itsLength; }
46: virtual int GetWidth() { return
itsWidth; }
47: virtual void Draw();
48: private:
49: int itsWidth;
50: int itsLength;
51: };
52:
53: void Rectangle::Draw()
54: {
55: for (int i = 0; i<itsLength; i++)
56: {
57: for (int j = 0; j<itsWidth; j++)
58: cout << "x ";
59:
60: cout << "\n";
61: }
62: }
63:
64: class Square : public Rectangle
65: {
66: public:
67: Square(int len);
68: Square(int len, int width);
69: ~Square(){}
70: long GetPerim() {return 4 *
GetLength();}
71: };
72:
73: Square::Square(int len):
74: Rectangle(len,len)
75: {}
76:
77: Square::Square(int len, int width):
78: Rectangle(len,width)
79:
80: {
81: if (GetLength() != GetWidth())
82: cout << "Error, not a
square... a Rectangle??\n";
83: }
84:
85: int main()
86: {
87: int choice;
88: BOOL fQuit = FALSE;
89: Shape * sp;
90:
91: while (1)
92: {
93: cout << "(1)Circle
(2)Rectangle (3)Square (0)Quit: ";
94: cin >> choice;
95:
96: switch (choice)
97: {
98: case 1: sp = new Circle(5);
99: break;
100: case 2: sp = new Rectangle(4,6);
101: break;
102: case 3: sp = new Square(5);
103: break;
104: default: fQuit = TRUE;
105: break;
106: }
107: if (fQuit)
108: break;
109:
110: sp->Draw();
111: cout << "\n";
112: }
113: return 0;
114: }
Output:
(1)Circle (2)Rectangle (3)Square (0)Quit: 2
x x x x x x
x x x x x x
x x x x x x
x x x x x
x
(1)Circle
(2)Rectangle (3)Square (0)Quit:3
x x x x x
x x x x x
x x x x x
x x x x x
x x x x x
(1)Circle
(2)Rectangle (3)Square (0)Quit:0
Analysis:
On lines 7-16, the Shape class is declared. The GetArea() and GetPerim()
methods return an error value, and Draw() takes no action. After all, what does
it mean to draw a Shape? Only types of shapes (circles, rectangle, and so on)
can be drawn, Shapes as an abstraction cannot be drawn.
Circle
derives from Shape and overrides the three virtual methods. Note that there is
no reason to add the word "virtual," as that is part of their
inheritance. But there is no harm in doing so either, as shown in the Rectangle
class on lines 43, 44, and 47. It is a good idea to include the term virtual as
a reminder, a form of documentation.
Square
derives from Rectangle, and it too overrides the GetPerim() method, inheriting
the rest of the methods defined in Rectangle.
It is troubling,
though, that a client might try to instantiate a Shape object, and it might be
desirable to make that impossible. The Shape class exists only to provide an
interface for the classes derived from it; as such it is an Abstract Data Type,
or ADT.
--------------------------------------------------------------------------------
New Term:
An Abstract Data Type represents a concept (like shape) rather than an object
(like circle). In C++, an ADT is always the base class to other classes, and it
is not valid to make an instance of an ADT.
--------------------------------------------------------------------------------
C++
supports the creation of abstract data types with pure virtual functions. A
virtual function ismade pure by initializing it with zero, as in
virtual
void Draw() = 0;
32: int itsRadius;
33: int itsCircumference;
34: };
35:
36: void Circle::Draw()
37: {
38: cout << "Circle drawing
routine here!\n";
39: Shape::Draw();
40: }
41:
42:
43: class Rectangle : public Shape
44: {
45: public:
46: Rectangle(int len, int width):
47: itsLength(len), itsWidth(width){}
Any class
with one or more pure virtual functions is an ADT, and it is illegal to
instantiate an object of a class that is an ADT. Trying to do so will cause a
compile-time error. Putting a pure virtual function in your class signals two
things to clients of your class:
Don't make
an object of this class, derive from it.
Make sure
you override the pure virtual function.
Any class
that derives from an ADT inherits the pure virtual function as pure, and so
must override every pure virtual function if it wants to instantiate objects.
Thus, if Rectangle inherits from Shape, and Shape has three pure virtual
functions, Rectangle must override all three or it too will be an ADT. Listing
13.8 rewrites the Shape class to be an abstract data type. To save space, the
rest of Listing 13.7 is not reproduced here. Replace the declaration of Shape
in Listing 13.7, lines 7-16, with the declaration of Shape in Listing 13.8 and
run the program again.
1: class Shape
2: {
3: public:
4: Shape(){}
5: ~Shape(){}
6: virtual long GetArea() = 0; // error
7: virtual long GetPerim()= 0;
8: virtual void Draw() = 0;
9: private:
10: };
Output:
(1)Circle (2)Rectangle (3)Square (0)Quit: 2
x x x x x x
x x x x x x
x x x x x x
x x x x x x
(1)Circle
(2)Rectangle (3)Square (0)Quit: 3
x x x x x
x x x x x
x x x x x
x x x x x
x x x x x
(1)Circle
(2)Rectangle (3)Square (0)Quit: 0
Analysis:
As you can see, the workings of the program are totally unaffected. The only
difference is that it would now be impossible to make an object of class Shape.
Declare a
class to be an abstract data type by including one or more pure virtual
functions in the class declaration. Declare a pure virtual function by writing
= 0 after the function declaration. Example:
class Shape
{
virtual void
Draw() = 0; // pure virtual
};
Typically,
the pure virtual functions in an abstract base class are never implemented.
Because no objects of that type are ever created, there is no reason to provide
implementations, and the ADT works purely as the definition of an interface to
objects which derive from it.
It is
possible, however, to provide an implementation to a pure virtual function. The
function can then be called by objects derived from the ADT, perhaps to provide
common functionality to all the overridden functions. Listing 13.9 reproduces
Listing 13.7, this time with Shape as an ADT and with an implementation for the
pure virtual function Draw(). The Circle class overrides Draw(), as it must,
but it then chains up to the base class function for additional functionality.
In this
example, the additional functionality is simply an additional message printed,
but one can imagine that the base class provides a shared drawing mechanism,
perhaps setting up a window that all derived classes will use.
Listing 13.9. Implementing pure virtual
functions.
1: //Implementing pure virtual functions
2:
3: #include <iostream.h>
4:
5: enum BOOL { FALSE, TRUE };
6:
7: class Shape
8: {
9: public:
10: Shape(){}
11: ~Shape(){}
12: virtual long GetArea() = 0; // error
13: virtual long GetPerim()= 0;
14: virtual void Draw() = 0;
15: private:
16: };
17:
18: void Shape::Draw()
19: {
20: cout << "Abstract drawing
mechanism!\n";
21: }
22:
23: class Circle : public Shape
24: {
25: public:
26: Circle(int
radius):itsRadius(radius){}
27: ~Circle(){}
28: long GetArea() { return 3 * itsRadius
* itsRadius; }
29: long GetPerim() { return 9 *
itsRadius; }
30: void Draw();
31: private:
32: int itsRadius;
33: int itsCircumference;
34: };
35:
36: void Circle::Draw()
37: {
38: cout << "Circle drawing
routine here!\n";
39: Shape::Draw();
40: }
41:
42:
43: class Rectangle : public Shape
44: {
45: public:
46: Rectangle(int len, int width):
47: itsLength(len), itsWidth(width){}
48: ~Rectangle(){}
49: long GetArea() { return itsLength *
itsWidth; }
50: long GetPerim() {return 2*itsLength +
2*itsWidth; }
51: virtual int GetLength() { return
itsLength; }
52: virtual int GetWidth() { return
itsWidth; }
53: void Draw();
54: private:
55: int itsWidth;
56: int itsLength;
57: };
58:
59: void Rectangle::Draw()
60: {
61: for (int i = 0; i<itsLength; i++)
62: {
63: for (int j = 0; j<itsWidth; j++)
64: cout << "x ";
65:
66: cout << "\n";
67: }
68: Shape::Draw();
69: }
70:
71:
72: class Square : public Rectangle
73: {
74: public:
75: Square(int len);
76: Square(int len, int width);
77: ~Square(){}
78: long GetPerim() {return 4 *
GetLength();}
79: };
80:
81: Square::Square(int len):
82: Rectangle(len,len)
83: {}
84:
85: Square::Square(int len, int width):
86: Rectangle(len,width)
87:
88: {
89: if (GetLength() != GetWidth())
90: cout << "Error, not a
square... a Rectangle??\n";
91: }
92:
93: int main()
94: {
95: int choice;
96: BOOL fQuit = FALSE;
97: Shape * sp;
98:
99: while (1)
100: {
101: cout << "(1)Circle
(2)Rectangle (3)Square (0)Quit: ";
102: cin >> choice;
103:
104: switch (choice)
105: {
106: case 1: sp = new Circle(5);
107: break;
108: case 2: sp = new Rectangle(4,6);
109: break;
110: case 3: sp = new Square (5);
111: break;
112: default: fQuit = TRUE;
113: break;
114: }
115: if (fQuit)
116: break;
117:
118: sp->Draw();
119: cout << "\n";
120: }
121: return 0;
122: }
Output: (1)Circle
(2)Rectangle (3)Square (0)Quit: 2
x x x x x x
x x x x x x
x x x x x x
x x x x x x
Abstract drawing
mechanism!
(1)Circle
(2)Rectangle (3)Square (0)Quit: 3
x x x x x
x x x x x
x x x x x
x x x x x
x x x x x
Abstract drawing
mechanism!
(1)Circle
(2)Rectangle (3)Square (0)Quit: 0
Analysis:
On lines 7-16, the abstract data type Shape is declared, with all three of its
accessor methods declared to be pure virtual. Note that this is not necessary.
If any one were declared pure virtual, the class would have been an ADT.
The
GetArea() and GetPerim() methods are not implemented, but Draw() is. Circle and
Rectangle both override Draw(), and both chain up to the base method, taking
advantage of shared functionality in the base class.
Complex
Hierarchies of Abstraction
At times,
you will derive ADTs from other ADTs. It may be that you will want to make some
of the derived pure virtual functions non-pure, and leave others pure.
If you
create the Animal class, you may make Eat(), Sleep(), Move(), and Reproduce()
all be pure virtual functions. Perhaps from Animal you derive Mammal and Fish.
On
examination, you decide that every Mammal will reproduce in the same way, and
so you make Mammal::Reproduce() be non-pure, but you leave Eat(), Sleep(), and
Move() as pure virtual functions.
From Mammal
you derive Dog, and Dog must override and implement the three remaining pure
virtual functions so that you can make objects of type Dog.
What you've
said, as class designer, is that no Animals or Mammals can be instantiated, but
that all Mammals may inherit the provided Reproduce() method without overriding
it.
Listing
13.10 illustrates this technique with a bare-bones implementation of these
classes.
1: // Listing 13.10
2: // Deriving ADTs from other ADTs
3: #include <iostream.h>
4:
5: enum COLOR { Red, Green, Blue, Yellow,
White, Black, Brown } ;
6: enum BOOL { FALSE, TRUE };
7:
8: class Animal // common base to both horse and bird
9: {
10: public:
11: Animal(int);
12: virtual ~Animal() { cout <<
"Animal destructor...\n"; }
13: virtual int GetAge() const { return
itsAge; }
14: virtual void SetAge(int age) { itsAge =
age; }
15: virtual void Sleep() const = 0;
16: virtual void Eat() const = 0;
17: virtual void Reproduce() const = 0;
18: virtual void Move() const = 0;
19: virtual void Speak() const = 0;
20: private:
21: int itsAge;
22: };
23:
24: Animal::Animal(int age):
25: itsAge(age)
26: {
27: cout << "Animal
constructor...\n";
28: }
29:
30: class Mammal : public Animal
31: {
32: public:
33: Mammal(int age):Animal(age)
34: { cout << "Mammal
constructor...\n";}
35: ~Mammal() { cout << "Mammal destructor...\n";}
36: virtual void Reproduce() const
37: { cout << "Mammal
reproduction depicted...\n"; }
38: };
39:
40: class Fish : public Animal
41: {
42: public:
43: Fish(int age):Animal(age)
44: { cout << "Fish
constructor...\n";}
45: virtual ~Fish() {cout <<
"Fish destructor...\n"; }
46: virtual void Sleep() const { cout
<< "fish snoring...\n"; }
47: virtual void Eat() const { cout <<
"fish feeding...\n"; }
48: virtual void Reproduce() const
49: { cout << "fish laying
eggs...\n"; }
50: virtual void Move() const
51: { cout << "fish
swimming...\n"; }
52: virtual void Speak() const { }
53: };
54:
55: class Horse : public Mammal
56: {
57: public:
58: Horse(int age, COLOR color ):
59: Mammal(age), itsColor(color)
60: { cout << "Horse
constructor...\n"; }
61: virtual ~Horse() { cout <<
"Horse destructor...\n"; }
62: virtual void Speak()const { cout
<< "Whinny!... \n"; }
63: virtual COLOR GetItsColor() const {
return itsColor; }
64: virtual void Sleep() const
65: { cout << "Horse
snoring...\n"; }
66: virtual void Eat() const { cout <<
"Horse feeding...\n"; }
67: virtual void Move() const { cout
<< "Horse running...\n";}
68:
69: protected:
70: COLOR itsColor;
71: };
72:
73: class Dog : public Mammal
74: {
75: public:
76: Dog(int age, COLOR color ):
77: Mammal(age), itsColor(color)
78: { cout << "Dog
constructor...\n"; }
79: virtual ~Dog() { cout << "Dog
destructor...\n"; }
80: virtual void Speak()const { cout
<< "Whoof!... \n"; }
81: virtual void Sleep() const { cout
<< "Dog snoring...\n"; }
82: virtual void Eat() const { cout <<
"Dog eating...\n"; }
83: virtual void Move() const { cout << "Dog running...\n";
}
84: virtual void Reproduce() const
85: { cout << "Dogs
reproducing...\n"; }
86:
87: protected:
88: COLOR itsColor;
89: };
90:
91: int main()
92: {
93: Animal *pAnimal=0;
94: int choice;
95: BOOL fQuit = FALSE;
96:
97: while (1)
98: {
99: cout << "(1)Dog (2)Horse
(3)Fish (0)Quit: ";
100: cin >> choice;
101:
102: switch (choice)
103: {
104: case 1: pAnimal = new
Dog(5,Brown);
105: break;
106: case 2: pAnimal = new
Horse(4,Black);
107: break;
108: case 3: pAnimal = new Fish (5);
109: break;
110: default: fQuit = TRUE;
111: break;
112: }
113: if (fQuit)
114: break;
115:
116: pAnimal->Speak();
117: pAnimal->Eat();
118: pAnimal->Reproduce();
119: pAnimal->Move();
120: pAnimal->Sleep();
121: delete pAnimal;
122: cout << "\n";
123: }
124: return 0
125: }
Output:
(1)Dog (2)Horse (3)Bird (0)Quit: 1
Animal
constructor...
Mammal
constructor...
Dog
constructor...
Whoof!...
Dog eating...
Dog
reproducing....
Dog
running...
Dog
snoring...
Dog
destructor...
Mammal
destructor...
Animal
destructor...
(1)Dog
(2)Horse (3)Bird (0)Quit: 0
Analysis:
On lines 8-22, the abstract data type Animal is declared. Animal has non-pure
virtual accessors for itsAge, which are shared by all Animal objects. It has
five pure virtual functions, Sleep(), Eat(), Reproduce(), Move(), and Speak().
Mammal is
derived from Animal, is declared on lines 30-38, and adds no data. It overrides
Reproduce(), however, providing a common form of reproduction for all mammals.
Fish must override Reproduce(), because Fish derives directly from Animal and
cannot take advantage of Mammalian reproduction (and a good thing, too!).
Mammal
classes no longer have to override the Reproduce() function, but they are free
to do so if they choose, as Dog does on line 84. Fish, Horse, and Dog all
override the remaining pure virtual functions, so that objects of their type
can be instantiated.
In the body
of the program, an Animal pointer is used to point to the various derived
objects in turn. The virtual methods are invoked, and based on the runtime
binding of the pointer, the correct method is called in the derived class.
It would be
a compile-time error to try to instantiate an Animal or a Mammal, as both are
abstract data types.
In one
program, the class Animal is abstract, in another it is not. What determines
whether to make a class abstract or not?
The answer
to this question is decided not by any real-world intrinsic factor, but by what
makes sense in your program. If you are writing a program that depicts a farm
or a zoo, you may want Animal to be an abstract data type, but Dog to be a
class from which you can instantiate objects.
On the
other hand, if you are making an animated kennel, you may want to keep Dog as
an abstract data type, and only instantiate types of dogs: retrievers,
terriers, and so fort. The level of abstraction is a function of how finely you
need to distinguish your types.
--------------------------------------------------------------------------------
DO use
abstract data types to provide common functionality for a number of related
classes. DO override all pure virtual functions. DO make pure virtual any
function that must be overridden. DON'T try to instantiate an object of an
abstract data type.
--------------------------------------------------------------------------------
A very hot
trend in C++ is the creation and dissemination of design patterns. These are
well- documented solutions to common problems encountered by C++ programmers.
As an example, the observer pattern solves a common problem in inheritance.
Imagine you
develop a timer class which knows how to count elapsed seconds. Such a class might
have a class member itsSeconds which is an integer, and it would have methods
to set, get, and increment itsSeconds.
Now let's
further assume that your program wants to be informed every time the timer's
itsSeconds member is incremented. One obvious solution would be to put a
notification method into the timer. However, notification is not an intrinsic
part of timing, and the complex code for registering those classes which need
to be informed when the clock increments doesn't really belong in your timer
class.
More
importantly, once you work out the logic of registering those who are
interested in these changes, and then notifying them, you'd like to abstract
this out into a class of its own and be able to reuse it with other classes
which might be "observed" in this way.
Therefore,
a better solution is to create an observer class. Make this observer an
Abstract Data Type with a pure virtual function Update().
Now create
a second abstract data type, called Subject. Subject keeps an array of Observer
objects and also provides two methods: register() (which adds observers to its
list) and Notify(), which is called when there is something to report.
Those
classes which wish to be notified of your timer's changes inherit from
Observer. The timer itself inherits from Subject. The Observer class registers
itself with the Subject class. The Subject class calls Notify when it changes
(in this case when the timer updates).
Finally, we
note that not every client of timer wants to be observable, and thus we create
a new class called ObservedTimer, which inherits both from timer and from
Subject. This gives the ObservedTimer the timer characteristics and the ability
to be observed.
Many C++
programmers are aware that Java was based in large part on C++, and yet the
creators of Java chose to leave out multiple inheritance. It was their opinion
that multiple inheritance introduced complexity that worked against the ease of
use of Java. They felt they could meet 90% of the multiple inheritance
functionality using what are called interfaces.
--------------------------------------------------------------------------------
New Term:
An interface is much like an Abstract Data Type in that it defines a set of
functions that can only be implemented in a derived class. However, with
interfaces, you don't directly derive from the interface, you derive from
another class and implement the interface, much like multiple inheritance.
Thus, this marriage of an abstract data type and multiple inheritance gives you
something akin to a capability class without the complexity or overhead of
multiple inheritance. In addition, because interfaces cannot have
implementations nor data members, the need for virtual inheritance is
eliminated.
--------------------------------------------------------------------------------
Whether
this is a bug or a feature is in the eyes of the beholder. In either case, if
you understand multiple inheritance and Abstract Data Types in C++ you will be
in a good position to move on to using some of the more advanced features of
Java should you decide to learn that language as well.
The
observer pattern and how it is implemented both in Java and C++ is covered in
detail in Robert Martin's article "C++ and Java: A Critical
Comparison," in the January 1997 issue of C++ Report.
Today you
learned how to overcome some of the limitations in single inheritance. You
learned about the danger of percolating interfaces up the inheritance hierarchy,
and the risks in casting down the inheritance hierarchy. You also learned how
to use multiple inheritance, what problems multiple inheritance can create and
how to solve them using virtual inheritance.
You also
learned what Abstract Data Types are and how to create Abstract classes using
pure virtual functions. You learned how to implement pure virtual functions and
when and why you might do so. Finally, you saw how to implement the Observer
Pattern using multiple inheritance and Abstract Data types.
Q. What
does percolating functionality upwards mean?
A. This
refers to the idea of moving shared functionality upwards into a common base
class. If more than one class shares a function, it is desirable to find a
common base class in which that function can be stored.
Q. Is
percolating upwards always a good thing?
A. Yes, if
you are percolating shared functionality upwards. No, if all you are moving is
interface. That is, if all the derived classes can't use the method, it is a
mistake to move it up into a common base class. If you do, you'll have to
switch on the runtime type of the object before deciding if you can invoke the
function.
Q. Why is
switching on the runtime type of an object bad?
A. With
large programs, the switch statements become big and hard to maintain. The
point of virtual functions is to let the virtual table, rather than the
programmer, determine the runtime type of the object.
Q. Why is
casting bad?
A. Casting
isn't bad if it is done in a way that is type-safe. If a function is called
that knows that the object must be of a particular type, casting to that type
is fine. Casting can be used to undermine the strong type checking in C++, and
that is what you want to avoid. If you are switching on the runtime type of the
object and then casting a pointer, that may be a warning sign that something is
wrong with your design.
Q. Why not
make all functions virtual?
A. Virtual
functions are supported by a virtual function table, which incurs runtime
overhead, both in the size of the program and in the performance of the
program. If you have very small classes that you don't expect to subclass, you
may not want to make any of the functions virtual.
Q. When
should the destructor be made virtual?
A. Anytime
you think the class will be subclassed, and a pointer to the base class will be
used to access an object of the subclass. As a general rule of thumb, if you've
made any functions in your class virtual, be sure to make the destructor
virtual as well.
Q. Why
bother making an Abstract Data Type--why not just make it non-abstract and
avoid creating any objects of that type?
A. The
purpose of many of the conventions in C++ is to enlist the compiler in finding
bugs, so as to avoid runtime bugs in code that you give your customers. Making
a class abstract, that is, giving it pure virtual functions, causes the
compiler to flag any objects created of that abstract type as errors.
The
Workshop provides quiz questions to help you solidify your understanding of the
material covered, and exercises to provide you with experience in using what
you've learned. Try to answer the quiz and exercise questions before checking
the answers in Appendix D, and make sure you understand the answers before
continuing to the next chapter.
1. What is
a down cast?
2. What is
the v-ptr?
3. If a
round-rectangle has straight edges and rounded corners, and your RoundRect
class inherits both from Rectangle and from Circle, and they in turn both
inherit from Shape, how many Shapes are created when you create a RoundRect?
4. If Horse
and Bird inherit from Animal using public virtual inheritance, do their
constructors initialize the Animal constructor? If Pegasus inherits from both
Horse and Bird, how does it initialize Animal's constructor?
5. Declare
a class vehicle, and make it an abstract data type.
6. If a
base class is an ADT, and it has three pure virtual functions, how many of
these must be overridden in its derived classes?
1. Show the
declaration for a class JetPlane, which inherits from Rocket and Airplane.
2. Show the
declaration for 747, which inherits from the JetPlane class described in
Exercise 1.
3. Write a
program that derives Car and Bus from the class Vehicle. Make Vehicle be an ADT
with two pure virtual functions. Make Car and Bus not be ADTs.
4. Modify
the program in Exercise 3 so that Car is an ADT, and derive SportsCar, Wagon,
and Coupe from Car. In the Car class, provide an implementation for one of the
pure virtual functions in Vehicle and make it non-pure.