Hour 17. Using Polymorphism and Derived Classes

Polymorphism Implemented with Virtual Methods

The previous hour emphasized the fact that a Dog object is a Mammal object. This meant that the Dog object inherited the attributes (data) and capabilities (member functions) of its base class. The relationship between a base class and derived class runs deeper than that in C++.

Polymorphism allows derived objects to be treated as if they were base objects. For example, suppose you create specialized Mammal types such as Dog, Cat, Horse, and so forth. All these derive from Mammal, and Mammal has a number of member functions factored out of the derived classes. One such function is speak(), which implements the capability of all mammals to make noise.

You’d like to teach each of the derived types to specialize how they speak. A dog says “woof,” a cat says “meow,” and so forth. Each class must be able to override how it implements the speak() method.

At the same time, when you have a collection of Mammal objects such as a Farm with Dog, Cat, Horse, and Cow objects, you want the farm to be able to tell each of these objects to speak() without knowing or caring about the details of how they implement the speak() method. When you treat these objects as if they are all mammals by calling the Mammal.speak() method, you are treating them polymorphically.


By the Way

Polymorphism is an unusual word that means the ability to take many forms. It comes from the roots poly, which means many, and morph, which means form. You are dealing with Mammal in its many forms.


You can use polymorphism to declare a pointer to Mammal and assign to it the address of a Dog object you create on the heap. Because a Dog “is a” Mammal, the following is perfectly legal:

Mammal* pMammal = new Dog;

You then can use this pointer to invoke any member function on Mammal. What you would like is for those functions that are overridden in Dog to call the correct function.

Virtual member functions let you do that. When you treat these objects polymorphically, you call the method on the Mammal pointer and you don’t know or care what the actual object is or how it implements its method.

The Mammal8 program in Listing 17.1 illustrates how virtual functions implement polymorphism.

Listing 17.1 The Full Text of Mammal8.cpp


 1: #include <iostream>
 2:
 3: class Mammal
 4: {
 5: public:
 6:     Mammal():age(1) { std::cout << "Mammal constructor ... "; }
 7:     ~Mammal() { std::cout << "Mammal destructor ... "; }
 8:     void move() const { std::cout << "Mammal, move one step "; }
 9:     virtual void speak() const { std::cout << "Mammal speak! "; }
10:
11: protected:
12:     int age;
13: };
14:
15: class Dog : public Mammal
16: {
17: public:
18:     Dog() { std::cout << "Dog constructor ... "; }
19:     ~Dog() { std::cout << "Dog destructor .. "; }
20:     void wagTail() { std::cout << "Wagging tail ... "; }
21:     void speak() const { std::cout << "Woof! "; }
22:     void move() const { std::cout << "Dog moves 5 steps ... "; }
23: };
24:
25: int main()
26: {
27:     Mammal *pDog = new Dog;
28:     pDog->move();
29:     pDog->speak();
30:     return 0;
31: }


Mammal8 displays this output:

Mammal constructor ...
Dog constructor ...
Mammal, move one step
Woof!

On line 9, Mammal is provided a virtual method called speak(). The designer of the class thereby signals that she expects this class to eventually be another class’s base type. The derived class will probably want to override this function.

On line 27, a pointer to Mammal is created, pDog, but it is assigned the address of a new Dog object. Because a Dog is a Mammal, this is a legal assignment. The pointer then is used to call the move() function. Because the compiler knows pDog only to be a Mammal, it looks to the Mammal object to find the move() function.

On line 29, the pointer then calls the speak() function. Because speak() is virtual, the speak() function overridden in Dog is invoked.

As far as the calling function knew, it had a Mammal pointer, but here a function of Dog was called. In fact, if you have an array of pointers to Mammal, each of which points to a subclass of Mammal, you can call each in turn and the correct function is called. The Mammal9 program (Listing 17.2) illustrates this idea.

Listing 17.2 The Full Text of Mammal9.cpp


 1: #include <iostream>
 2:
 3: class Mammal
 4: {
 5: public:
 6:     Mammal():age(1) {  }
 7:     ~Mammal() { }
 8:     virtual void speak() const { std::cout << "Mammal speak! "; }
 9: protected:
10:     int age;
11: };
12:
13: class Dog : public Mammal
14: {
15: public:
16:     void speak() const { std::cout << "Woof! "; }
17: };
18:
19: class Cat : public Mammal
20: {
21: public:
22:     void speak() const { std::cout << "Meow! "; }
23: };
24:
25: class Horse : public Mammal
26: {
27: public:
28:     void speak() const { std::cout << "Whinny! "; }
29: };
30:
31: class Pig : public Mammal
32: {
33: public:
34:     void speak() const { std::cout << "Oink! "; }
35: };
36:
37: int main()
38: {
39:     Mammal* array[5];
40:     Mammal* ptr;
41:     int choice, i;
42:     for (i = 0; i < 5; i++)
43:     {
44:         std::cout << "(1) dog (2) cat (3) horse (4) pig: ";
45:         std::cin >> choice;
46:         switch (choice)
47:         {
48:         case 1:
49:             ptr = new Dog;
50:             break;
51:         case 2:
52:             ptr = new Cat;
53:             break;
54:         case 3:
55:             ptr = new Horse;
56:             break;
57:         case 4:
58:             ptr = new Pig;
59:             break;
60:         default:
61:             ptr = new Mammal;
62:             break;
63:         }
64:         array[i] = ptr;
65:     }
66:     for (i=0; i < 5; i++)
67:     {
68:         array[i]->speak();
69:     }
70:     return 0;
71: }


Here’s sample output for Mammal9:

(1) dog (2) cat (3) horse (4) pig: 1
(1) dog (2) cat (3) horse (4) pig: 2
(1) dog (2) cat (3) horse (4) pig: 3
(1) dog (2) cat (3) horse (4) pig: 4
(1) dog (2) cat (3) horse (4) pig: 5
Woof!
Meow!
Whinny!
Oink!
Mammal speak!

This stripped-down program, which provides only the barest functionality to each class, illustrates virtual member functions in their purest form. Four classes are declared (Dog, Cat, Horse, and Pig), all derived from Mammal.

On line 8, Mammal’s speak() function is declared to be virtual. On lines 16, 22, 28, and 34, the four derived classes override the implementation of speak().

The user is prompted to pick which objects to create, and the pointers are added to the array in lines 42–65.


By the Way

Note that at compile time it is impossible to know which objects will be created, and therefore, which speak() methods will be invoked. The pointer ptr is bound to its object at runtime. This is called late binding, or sometimes runtime binding, as opposed to static binding, or compile-time binding.


How Virtual Member Functions Work

When a derived object, such as a Dog object, is created, first the constructor for the base class is called, and then the constructor for the derived class is called. Figure 17.1 shows what the Dog object looks like after it is created. Note that the Mammal part of the object is contiguous in memory with the Dog part.

Figure 17.1 The Dog object after it is created.

image

When a virtual function is created in an object, the object must keep track of that function. Many compilers build a virtual function table, called a v-table. One of these is kept for each type, and each object of that type keeps a virtual table pointer (called a vptr or v-pointer), which points to that table.

Although implementations vary, all compilers must accomplish the same thing, so you won’t be too wrong with this description.

Each object’s vptr points to the v-table that, in turn, has a pointer to each of the virtual member functions. When the Mammal part of the Dog is created, the vptr is initialized to point to the virtual methods for Mammal, as shown in Figure 17.2.

Figure 17.2 The v-table of a Mammal.

image

When the Dog constructor is called and the Dog part of this object is added, the vptr is adjusted to point to the virtual function overrides (if any) in the Dog object, as illustrated in Figure 17.3.

Figure 17.3 The v-table of a Dog.

image

When a pointer to a Mammal is used, the vptr continues to point to the correct function, depending on the real type of the object. Thus, when speak() is invoked, the correct function is invoked.

You Can’t Get There from Here

If the Dog object had a member function called wagTail() that was not in the Mammal class, you could not use the pointer to Mammal to access that function (unless you cast it to be a pointer to Dog). Because wagTail() is not a virtual function and is not in a Mammal object, you can’t get there without either a Dog object or a Dog pointer.

Although you can transform the Mammal pointer into a Dog pointer, there usually are better and safer ways to call the wagTail() method. C++ frowns on explicit casts because they are error-prone. This subject is addressed in depth when multiple-inheritance is covered in Hour 18, “Making Use of Advanced Polymorphism,” and again when templates are covered in Hour 24, “Dealing with Exceptions and Error Handling.”

Slicing

Note that the virtual function magic only operates on pointers and references. Passing an object by value will not enable the virtual member functions to be invoked. The Mammal10 program in Listing 17.3 illustrates this problem.

Listing 17.3 The Full Text of Mammal10.cpp


 1: #include <iostream>
 2:
 3: class Mammal
 4: {
 5: public:
 6:    Mammal():age(1) { }
 7:     ~Mammal() { }
 8:     virtual void speak() const { std::cout << "Mammal speak! "; }
 9: protected:
10:     int age;
11: };
12:
13: class Dog : public Mammal
14: {
15: public:
16:     void speak() const { std::cout << "Woof! "; }
17: };
18:
19: class Cat : public Mammal
20: {
21: public:
22:     void speak()const { std::cout << "Meow! "; }
23: };
24:
25: void valueFunction(Mammal);
26: void ptrFunction(Mammal*);
27: void refFunction(Mammal&);
28:
29: int main()
30: {
31:     Mammal* ptr=0;
32:     int choice;
33:     while (1)
34:     {
35:         bool fQuit = false;
36:         std::cout << "(1) dog (2) cat (0) quit: ";
37:         std::cin >> choice;
38:         switch (choice)
39:         {
40:         case 0:
41:             fQuit = true;
42:             break;
43:         case 1:
44:             ptr = new Dog;
45:             break;
46:         case 2:
47:             ptr = new Cat;
48:             break;
49:         default:
50:             ptr = new Mammal;
51:             break;
52:         }
53:         if (fQuit)
54:         {
55:             break;
56:         }
57:         ptrFunction(ptr);
58:         refFunction(*ptr);
59:         valueFunction(*ptr);
60:     }
61:     return 0;
62: }
63:
64: void valueFunction(Mammal mammalValue)  // This function is called last
65: {
66:     mammalValue.speak();
67: }
68:
69: void ptrFunction (Mammal *pMammal)
70: {
71:     pMammal->speak();
72: }
73:
74: void refFunction (Mammal &rMammal)
75: {
76:     rMammal.speak();
77: }


Here’s a sample run and the corresponding output:

(1) dog (2) cat (0) quit: 1
Woof!
Woof!
Mammal speak!
(1) dog (2) cat (0) quit: 2
Meow!
Meow!
Mammal speak!
(1)dog (2)cat (0)Quit: 0

On lines 3–23, stripped-down versions of the Mammal, Dog, and Cat classes are declared. Three functions are declared: ptrFunction(), refFunction(), and valueFunction(). They take a pointer to a Mammal, a Mammal reference, and a Mammal object, respectively. All three functions then do the same thing; they call the speak() method.

The user is prompted to choose a Dog or Cat; based on the choice she makes, a pointer to the correct type is created on lines 38–52.

In the first line of the output, the user chooses Dog. The Dog object is created on the heap in line 44. The Dog then is passed as a pointer, as a reference, and by value to the three functions. The pointer and references all invoke the virtual member functions, and the Dog->speak() member function is invoked. This is shown on the first two lines of output after the user’s choice.

The dereferenced pointer is passed by value, however. The function expects a Mammal object, so the compiler slices down the Dog object to just the Mammal part. At that point, the Mammal speak() method is called, as reflected in the third line of output after the user’s choice.

This experiment then is repeated for the Cat object, with similar results.

Virtual Destructors

It is legal and common to pass a pointer to a derived object when a pointer to a base object is expected. What happens when that pointer to a derived subject is deleted? If the destructor is virtual, as it should be, the right thing happens—the derived class’s destructor is called. Because the derived class’s destructor will automatically invoke the base class’s destructor, the entire object will be properly destroyed.

The rule of thumb is this: If any of the functions in your class are virtual, the destructor should also be virtual.

Virtual Copy Constructors

As previously stated, no constructor can be virtual. Nonetheless, there are times when your program desperately needs to be able to pass in a pointer to a base object and have a copy of the correct derived object that is created. A common solution to this problem is to create a clone member function in the base class and to make it virtual. A clone function creates a new copy of the current object and returns that object.

Because each derived class overrides the clone function, a copy of the derived class is created. The Mammal11 program (Listing 17.4) illustrates how this is used.

Listing 17.4 The Full Text of Mammal11.cpp


 1: #include <iostream>
 2:
 3: class Mammal
 4: {
 5: public:
 6:     Mammal():age(1) { std::cout << "Mammal constructor ... "; }
 7:     virtual ~Mammal() { std::cout << "Mammal destructor ... "; }
 8:     Mammal (const Mammal &rhs);
 9:     virtual void speak() const { std::cout << "Mammal speak! "; }
10:     virtual Mammal* clone() { return new Mammal(*this); }
11:     int getAge() const { return age; }
12:
13: protected:
14:     int age;
15: };
16:
17: Mammal::Mammal (const Mammal &rhs):age(rhs.getAge())
18: {
19:     std::cout << "Mammal copy constructor ... ";
20: }
21:
22: class Dog : public Mammal
23: {
24: public:
25:     Dog() { std::cout << "Dog constructor ... "; }
26:     virtual ~Dog() { std::cout << "Dog destructor ... "; }
27:     Dog (const Dog &rhs);
28:     void speak() const { std::cout << "Woof! "; }
29:     virtual Mammal* clone() { return new Dog(*this); }
30: };
31:
32: Dog::Dog(const Dog &rhs):
33: Mammal(rhs)
34: {
35:     std::cout << "Dog copy constructor ... ";
36: }
37:
38: class Cat : public Mammal
39: {
40: public:
41:     Cat() { std::cout << "Cat constructor ... "; }
42:     virtual ~Cat() { std::cout << "Cat destructor ... "; }
43:     Cat (const Cat&);
44:     void speak() const { std::cout << "Meow! "; }
45:     virtual Mammal* Clone() { return new Cat(*this); }
46: };
47:
48: Cat::Cat(const Cat &rhs):
49: Mammal(rhs)
50: {
51:     std::cout << "Cat copy constructor ... ";
52: }
53:
54: enum ANIMALS { MAMMAL, DOG, CAT};
55: const int numAnimalTypes = 3;
56: int main()
57: {
58:     Mammal *array[numAnimalTypes];
59:     Mammal *ptr;
60:     int choice, i;
61:     for (i = 0; i < numAnimalTypes; i++)
62:     {
63:         std::cout << "(1) dog (2) cat (3) mammal: ";
64:         std::cin >> choice;
65:         switch (choice)
66:         {
67:         case DOG:
68:             ptr = new Dog;
69:             break;
70:         case CAT:
71:             ptr = new Cat;
72:             break;
73:         default:
74:             ptr = new Mammal;
75:             break;
76:         }
77:         array[i] = ptr;
78:     }
79:     Mammal *otherArray[numAnimalTypes];
80:     for (i=0; i < numAnimalTypes; i++)
81:     {
82:         array[i]->speak();
83:         otherArray[i] = array[i]->clone();
84:     }
85:     for (i=0; i < numAnimalTypes; i++)
86:     {
87:         otherArray[i]->speak();
88:     }
89:     return 0;
90: }


The following output demonstrates one run of the program:

1:  (1) dog (2) cat (3) mammal: 1
2:  Mammal constructor...
3:  Dog constructor...
4:  (1) dog (2) cat (3) mammal: 2
5:  Mammal constructor...
6:  Cat constructor...
7:  (1) dog (2) cat (3) mammal: 3
8:  Mammal constructor...
9:  Woof!
10: Mammal copy constructor...
11: Dog copy constructor...
12: Meow!
13: Mammal copy constructor...
14: Cat copy constructor...
15: Mammal speak!
16: Mammal copy constructor...
17: Woof!
18: Meow!
19: Mammal speak!

Listing 17.4 is similar to the previous two listings, except that a new virtual function has been added to the Mammal class: clone(). This function returns a pointer to a new Mammal object by calling the copy constructor, passing in itself (*this) as a const reference.

Dog and Cat both override the clone() function, initializing their data and passing in copies of themselves to their own copy constructors. Because clone() is virtual, this effectively creates a virtual copy constructor, as shown on line 83.

The user is prompted to choose dogs, cats, or mammals, and these are created on lines 65–76. A pointer to each choice is stored in an array on line 77.

As the program iterates over the array, each object has its speak() and its clone() method called, in turn, on lines 82 and 83. The result of the clone() call is a pointer to a copy of the object, which then is stored in a second array on line 83.

On line 1 of the output, the user is prompted and responds with 1, choosing to create a dog. The Mammal and Dog constructors are invoked. This is repeated for Cat and for Mammal on lines 4–8 of the output.

Line 9 of the output represents the call to speak() on the first object, the Dog from line 82 (within the first for loop). The virtual speak() method is called, and the correct version of speak() is invoked. The clone() function then is called, and as this is also virtual, Dog’s clone function is invoked, causing the Mammal constructor and the Dog copy constructor to be called.

The same is repeated for Cat on lines 12–14 of the output, and then for Mammal on lines 15 and 16. Finally, the new array is iterated (output lines 17–19, code lines 85–88), and each of the new objects has speak() invoked.

The difference between this approach and the use of a copy constructor is that you, as the programmer, must explicitly call the clone() function. The copy constructor is called automatically when an object is copied. Remember that you can always override the copy function in a derived class. But that approach reduces the flexibility you have.

The Cost of Virtual Member Functions

Because objects with virtual member functions must maintain a v-table, some overhead is required to employ them. If you have a small class from which you do not expect to derive other classes, there might be no reason to have any virtual functions at all.

After you declare any functions virtual, you’ve paid most of the price of the v-table (although each entry does add a small memory overhead). At that point, you want the destructor to be virtual, and the assumption will be that all other functions probably will also be virtual. Take a long hard look at any nonvirtual functions, and be certain you understand why they are not virtual.

Summary

Polymorphism enables the same interface to be implemented with different member functions in a set of classes related by inheritance.

This makes it possible for related objects to be used in the same manner, even if each object implements the behavior differently.

Polymorphism achieves an important goal in object-oriented programming by letting similar objects handle related functionality by reusing an interface.

Q&A

Q. Why not make all class functions virtual?

A. There is overhead with the first virtual function in the creation of a v-table. After that, the overhead is trivial. Many C++ programmers feel that if one function is virtual, all others should be. Other programmers disagree, believing that there should always be a reason for what you do.

Q. If a function someFunc() is virtual in a base class and is also overloaded so as to take either an integer or two integers, and the derived class overrides the form taking one integer, what is called when a pointer to a derived object calls the two-integer form?

A. The overriding of the one int form hides the entire base class function; thus, you will get a compile error complaining that the function requires only one int.

Q. What is the origin of “Rudolph the Red-Nosed Reindeer”?

A. “Rudolph” began as a 1939 poem by Robert May, a 34-year- copywriter for the Montgomery Wards department store in Chicago. May’s boss wanted something to give children in the store, and it became so popular that five million copies were distributed in the ‘30s and ‘40s.

The poem was written when May’s wife was seriously ill. Like Rudolph, May’s four-year-old daughter felt left out—her mother couldn’t do things with her like other mothers could.

In 1949, Johnny Marks and singer Gene Autry recorded the song based on the poem, and it became one of the top-selling singles of all time.

The reindeer almost wasn’t named Rudolph. Two other names May proposed were Rollo and Reginald.

Workshop

You spent the past hour learning about polymorphism and derived classes. Now you should answer a few questions and do a couple of exercises to firm up that knowledge.

Quiz

1. How does a C++ program know which virtual function to call when the objects are stored in a variable of the base class type?

A. The function has a virtual keyword.

B. A v-table is used.

C. That’s not possible.

2. What type of method cannot be virtual?

A. Constructor

B. Desctructor

C. Clone

3. What is it called when a pointer is bound to an object at runtime, as in polymorphism?

A. Late binding

B. Static binding

C. Dereferencing

Answers

1. B. The v-table keeps track of this information for you. It is the overhead associated with this table that makes virtual functions slightly more expensive to use than regular functions.

2. A. The constructor (including the copy constructor).

3. A. Late binding is when it occurs at runtime. Static binding is when it occurs during compilation.

Activities

1. Modify the Mammal8 program by commenting out line 21: the speak() method within dog. Can you think of examples where it makes sense to do this?

2. Modify the Mammal10 program to remove the virtual on line 8 (definition of speak() in the base class)? Can you see why the override functions are never called?

To see solutions to these activities, visit the book’s website at http://cplusplus.cadenhead.org.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.17.27