Polymorphism Implemented With Virtual Methods

The previous chapter emphasized the fact that a Dog object is a Mammal object. So far that has meant only that the Dog object has inherited the attributes (data) and capabilities (methods) of its base class. In C++, the is-a relationship runs deeper than that, however.

Polymorphism allows you to treat derived objects as if they were base objects. For example, suppose you create a number of specialized Mammal types: Dog, Cat, Horse, and so forth. All of these derive from Mammal, and Mammal has a number of methods which are factored out of the derived classes. One such method might be Speak(). All mammals can make noise.

You'd like to teach each of the derived types to specialize how they speak. A Dog says “bow wow,” 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 (for example, a Farm with Dog, Cat, Horse and Cow objects), you want the Farm simulation 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. Poly means many, morph means form; you are dealing with Mammal in all its many forms.

You can, for example, declare a pointer to Mammal, and assign to it the address of a Dog object you create on the heap. Since a Dog is-a Mammal, the following is perfectly legal:

Mammal* pMammal = new Dog;

You can then use this pointer to invoke any method on Mammal. What you would like is for those methods 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 (in this case Dog) is or how it implements its method.

Listing 17.1 illustrates how virtual functions implement polymorphism.

Listing 17.1. Using Virtual Methods
 0:  //Listing 17.1 Using virtual methods
 1:  #include <iostream>
 2:
 3:  class Mammal
 4:  {
 5:  public:
 6:      Mammal():itsAge(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 itsAge;
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:  }


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

On line 9, Mammal is provided a virtual method—Speak(). The designer of this class thereby signals that he 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 is then 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() method.

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

This is almost magical—as far as the calling function knew, it had a Mammal pointer, but here a method on 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. Listing 17.2 illustrates this idea.

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


 (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!
Winnie!
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, 23, 30, and 36, 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 44–65.

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.


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.


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.

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.

Figure 17.3. The v-table of a Dog.


You Can't Get There from Here

If the Dog object had a method, WagTail(), that was not in the Mammal, you could not use the pointer to Mammal to access that method (unless you cast it to be a pointer to Dog). Because WagTail() is not a virtual function, and because it 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 are usually far 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, “Advanced Polymorphism,” and again when templates are covered in Hour 24, “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. Listing 17.3 illustrates this problem.

Listing 17.3. Data Slicing when Passing by Value
 0:  //Listing 17.3 Data slicing with passing by value
 1:  #include <iostream>
 2:
 3:  class Mammal
 4:  {
 5:  public:
 6:      Mammal():itsAge(1) {  }
 7:      ~Mammal() {}
 8:      virtual void Speak() const { std::cout << "Mammal speak!
"; }
 9:  protected:
10:      int itsAge;
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:
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:              break;
55:          PtrFunction(ptr);
56:          RefFunction(*ptr);
57:          ValueFunction(*ptr);
58:      }
59:      return 0;
60:  }
61:
62:  void ValueFunction (Mammal MammalValue)
63:  {
64:      MammalValue.Speak();
65:  }
66:
67:  void PtrFunction (Mammal * pMammal)
68:  {
69:      pMammal->Speak();
70:  }
71:
72:  void RefFunction (Mammal & rMammal)
73:  {
74:      rMammal.Speak();
75:  }


 (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 he 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 is then 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, however, is passed by value. 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 is then 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 be virtual as well.

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 method in the base class and to make it virtual. A clone method creates a new copy of the current object and returns that object.

Because each derived class overrides the clone method, a copy of the derived class is created. Listing 17.4 illustrates how this is used.

Listing 17.4. A Virtual Copy Constructor
 0:  //Listing 17.4 Virtual copy constructor
 1:  #include <iostream>
 2:
 3:  class Mammal
 4:  {
 5:  public:
 6:      Mammal():itsAge(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 itsAge; }
12:
13:  protected:
14:      int itsAge;
15:  };
16:
17:  Mammal::Mammal (const Mammal & rhs):itsAge(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 *theArray[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:          theArray[i] = ptr;
78:      }
79:      Mammal *OtherArray[NumAnimalTypes];
80:      for (i=0;i<NumAnimalTypes;i++)
81:      {
82:          theArray[i]->Speak();
83:          OtherArray[i] = theArray[i]->Clone();
84:      }
85:      for (i=0;i<NumAnimalTypes;i++)
86:          OtherArray[i]->Speak();
87:      return 0;
88:  }


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 very similar to the previous two listings, except that a new virtual method has been added to the Mammal class: clone(). This method 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() method, initializing their data and passing in copies of themselves to their own copy constructors. Because clone() is virtual, this will effectively create 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 is then 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. The virtual Speak() method is called, and the correct version of Speak() is invoked. The Clone() function is then called, and as this is also virtual, Dog's Clone method 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, and each of the new objects has Speak() invoked.

The Cost of Virtual Methods

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

After you declare any methods 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'll want the destructor to be virtual, and the assumption will be that all other methods probably will be virtual as well. Take a long hard look at any non-virtual methods, and be certain you understand why they are not virtual.

DO use virtual methods when you expect to derive from a class.
DO use a virtual destructor if any methods are virtual.
DON'T mark the constructor as virtual.


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

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