Hour 18. Making Use of Advanced Polymorphism

Problems with Single Inheritance

In the previous hours, we discussed treating derived objects polymorphically with their base classes. You saw that if the base class has a member function speak() that is overridden in the derived class, a pointer to a base object that is assigned to a derived object will do the right thing. The Mammal12 program in Listing 18.1 illustrates this idea.

Listing 18.1 The Full Text of Mammal12.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:     virtual void speak() const { std::cout << "Mammal speak! "; }
 9: protected:
10:     int age;
11: };
12:
13: class Cat : public Mammal
14: {
15: public:
16:     Cat() { std::cout << "Cat constructor ... "; }
17:     ~Cat() { std::cout << "Cat destructor ... "; }
18:     void speak() const { std::cout << "Meow! "; }
19: };
20:
21: int main()
22: {
23:     Mammal *pCat = new Cat;
24:     pCat->speak();
25:     return 0;
26: }


When you run the Mammal12 program, the following output displays:

Mammal constructor ...
Cat constructor ...
Meow!

On line 8, speak() is declared to be a virtual member function; it is overridden on line 18 and invoked on line 24. Note, again, that pCat is declared to be a pointer to Mammal, but the address of a Cat is assigned to it. As discussed in Hour 17, “Using Polymorphism and Derived Classes,” this is the essence of polymorphism.

What happens, however, if you want to add a member function to Cat that is inappropriate for Mammal? Suppose you want to add a function called purr(). Cats purr, but other mammals do not. You would declare your class like this:

class Cat: public Mammal
{
public:
    Cat() { std::cout << "Cat constructor ... "; }
    ~Cat() { std::cout << "Cat destructor ... "; }
    void speak() const { std::cout << "Meow! "; }
    void purr() const { std::cout << "Rrrrrrrr! "; }
};

The problem is this: If you now call purr() using your pointer to Mammal, you get a compiler error stating that “’Purr’ is not a member of Mammal.”

When your compiler tries to resolve purr() in its Mammal virtual table, there is no entry. You can percolate this function up into the base class, but that is a bad idea. Although it works as an expedient, populating your base class with functions that are specific to derived classes is poor programming practice and a recipe for difficult-to-maintain code.

In fact, this entire problem is a reflection of bad design. Generally, if you have a pointer to a base class that is assigned to a derived class object, it is because you intend to use that object polymorphically, and in this case, you ought not even try to access functions that are specific to the derived class.

The problem is not that you have such specific functions; it is that you are trying to get at them with the base class pointer. In an ideal world, when you have such a pointer you would not try to get at those functions.

But this is not an ideal world, and at times, you find yourself with a collection of base objects—for example, a zoo full of mammals. At one point or another, you might realize you have a Cat object and you want the darn thing to purr. In this case, there might be only one thing to do: cheat.

To cheat, cast your base class pointer to your derived type. You say to the compiler, “Look, compiler, I am the programmer and know this is really a cat, so go and do what I tell you.”

To make this work, you’ll use the dynamic_cast operator. This operator ensures that when you cast, you cast safely.

Here’s how it works: If you have a pointer to a base class, such as Mammal, and you assign to it a pointer to a derived class, such as Cat, you can use the Mammal pointer polymorphically to access virtual functions. Then, if you need to get at the Cat object to call, for example, the purr() method, you create a Cat pointer using the dynamic_cast operator to do so. At runtime, the base pointer is examined. If the conversion is proper, your new Cat pointer is fine. If the conversion is improper, if you didn’t really have a Cat object after all, your new pointer will be null. The Mammal13 program in Listing 18.2 illustrates this.

Listing 18.2 The Full Text of Mammal13.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:     virtual void speak() const { std::cout << "Mammal speak! "; }
 9: protected:
10:     int age;
11: };
12:
13: class Cat: public Mammal
14: {
15: public:
16:     Cat() { std::cout << "Cat constructor ... "; }
17:     ~Cat() { std::cout << "Cat destructor ... "; }
18:     void speak() const { std::cout << "Meow! "; }
19:     void purr() const { std::cout << "Rrrrrrrrrrr! "; }
20: };
21:
22: class Dog: public Mammal
23: {
24: public:
25:     Dog() { std::cout << "Dog constructor ... "; }
26:     ~Dog() { std::cout << "Dog destructor ... "; }
27:     void speak() const { std::cout << "Woof! "; }
28: };
29:
30: int main()
31: {
32:     const int numberMammals = 3;
33:     Mammal* zoo[numberMammals];
34:     Mammal* pMammal;
35:     int choice, i;
36:     for (i = 0; i < numberMammals; i++)
37:     {
38:         std::cout << "(1)Dog (2)Cat: ";
39:         std::cin >> choice;
40:         if (choice == 1)
41:             pMammal = new Dog;
42:         else
43:             pMammal = new Cat;
44:
45:         zoo[i] = pMammal;
46:     }
47:
48:     std::cout << " ";
49:
50:     for (i = 0; i < numberMammals; i++)
51:     {
52:         zoo[i]->speak();
53:
54:         Cat *pRealCat =  dynamic_cast<Cat *> (zoo[i]);
55:         if (pRealCat)
56:             pRealCat->purr();
57:         else
58:             std::cout << "Uh oh, not a cat! ";
59:
60:         delete zoo[i];
61:         std::cout << " ";
62:     }
63:
64:     return 0;
65: }


When you run Mammal13, you’re asked three times to create either a Dog object or Cat object. After the third response, each object is tested by calling either speak() alone or speak() and purr(). Here’s sample output:

(1)Dog (2)Cat: 1
Mammal constructor ...
Dog constructor ...
(1)Dog (2)Cat: 2
Mammal constructor ...
Cat constructor ...

(1)Dog (2)Cat: 1
Mammal constructor ...
Dog constructor ...

Woof!
Uh oh, not a cat!
Mammal destructor ...

Meow
rrrrrrrrrrr
Mammal destructor ...

Woof!
Uh oh, not a cat!
Mammal destructor ...

On lines 38–45, the user is asked to choose to add either a Cat or a Dog object to the array of Mammal pointers. Line 50 walks through the array and, on line 52, each object’s virtual speak() method is called. These functions respond polymorphically: Cats meow, and dogs say woof!

Cat objects should purr, but the purr() function must not be called on Dog objects. The dynamic_cast operator on line 54 ensures that the object is a Cat. When it is, the pointer will not equal null and passes the conditional test on line 55.

Abstract Data Types

Often, you will create a hierarchy of classes together. For example, you might create a Shape class as a base class to derive a Rectangle and a Circle. From Rectangle, you might derive Square as a special case of Rectangle.

Each of the derived classes override the draw() method, the getArea() method, and so forth. Listing 18.3 illustrates a bare-bones implementation of the Shape class and its derived Circle and Rectangle classes.

Listing 18.3 The Full Text of Shape.cpp


  1: #include <iostream>
  2:
  3: class Shape
  4: {
  5: public:
  6:     Shape() {}
  7:     virtual ~Shape() {}
  8:     virtual long getArea() { return -1; } // error
  9:     virtual long getPerim() { return -1; }
 10:     virtual void draw() {}
 11: };
 12:
 13: class Circle : public Shape
 14: {
 15: public:
 16:     Circle(int newRadius):radius(newRadius) {}
 17:     ~Circle() {}
 18:     long getArea() { return 3 * radius * radius; }
 19:     long getPerim() { return 9 * radius; }
 20:     void draw();
 21: private:
 22:     int radius;
 23:     int circumference;
 24: };
 25:
 26: void Circle::draw()
 27: {
 28:     std::cout << "Circle drawing routine here! ";
 29: }
 30:
 31: class Rectangle : public Shape
 32: {
 33: public:
 34:     Rectangle(int newLen, int newWidth):
 35:         length(newLen), width(newWidth) {}
 36:     virtual ~Rectangle() {}
 37:     virtual long getArea() { return length * width; }
 38:     virtual long getPerim() { return 2 * length + 2 * width; }
 39:     virtual int getLength() { return length; }
 40:     virtual int getWidth() { return width; }
 41:     virtual void draw();
 42: private:
 43:     int width;
 44:     int length;
 45: };
 46:
 47: void Rectangle::draw()
 48: {
 49:     for (int i = 0; i < length; i++)
 50:     {
 51:         for (int j = 0; j < width; j++)
 52:             std::cout << "x ";
 53:
 54:         std::cout << " ";
 55:     }
 56: }
 57:
 58: class Square : public Rectangle
 59: {
 60: public:
 61:     Square(int len);
 62:     Square(int len, int width);
 63:     ~Square() {}
 64:     long getPerim() { return 4 * getLength(); }
 65: };
 66:
 67: Square::Square(int newLen):
 68:     Rectangle(newLen, newLen)
 69: {}
 70:
 71: Square::Square(int newLen, int newWidth):
 72:     Rectangle(newLen, newWidth)
 73: {
 74:     if (getLength() != getWidth())
 75:         std::cout << "Error, not a square ... a rectangle? ";
 76: }
 77:
 78: int main()
 79: {
 80:     int choice;
 81:     bool fQuit = false;
 82:     Shape * sp;
 83:
 84:     while (1)
 85:     {
 86:         std::cout << "(1) Circle (2) Rectangle (3) Square (0) Quit: ";
 87:         std::cin >> choice;
 88:
 89:         switch (choice)
 90:         {
 91:         case 1:
 92:             sp = new Circle(5);
 93:             break;
 94:         case 2:
 95:             sp = new Rectangle(4, 6);
 96:             break;
 97:         case 3:
 98:             sp = new Square(5);
 99:             break;
100:         default:
101:             fQuit = true;
102:             break;
103:         }
104:         if (fQuit)
105:             break;
106:
107:         sp->draw();
108:         std::cout << " ";
109:     }
110:     return 0;
111: }


When run, this program asks the user one or more times to choose between creating a circle, rectangle or square. When 0 is chosen rather than a shape, it exits. Here’s a look at the output for a run:

(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

On lines 3–11, the Shape class is declared. The getArea() and getPerim() member functions return an error value, and draw() takes no action. Only specific types of shapes such as circles and rectangle can be drawn; shapes as an abstraction cannot be drawn.

Circle derives from Shape and overrides the three virtual member functions. Note that there is no reason to add the word virtual, because that is part of their inheritance. But there is no harm in doing so either, as shown in the Rectangle class on lines 36–41.

Square derives from Rectangle, and it too overrides the getPerim() member function, inheriting the rest of the functions defined in Rectangle.

It is troubling, though, that it is possible 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. It is an abstract data type, or ADT.

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. Therefore, if you make Shape an ADT, it is not possible to make an instance of a Shape object.

Pure Virtual Functions

C++ supports the creation of abstract data types with pure virtual functions. A pure virtual function is a virtual function that must be overridden in the derived class. A virtual function is made pure by initializing it with 0, as in the following:

virtual void draw() = 0;

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 causes 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 to 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. Therefore, if Rectangle inherits from Shape, and Shape has three pure virtual functions, Rectangle must override all three or it, too, will be an ADT.

A virtual function is declared to be abstract by writing = 0 after the function declaration, as in this statement:

virtual long getArea = 0;

Here’s a rewrite of the Shape class to be an abstract data type:

class Shape
{
public:
     Shape() {}
     virtual ~Shape() {}
     virtual long getArea() = 0;
     virtual long getPerim() = 0;
     virtual void draw() = 0;
private:
};

If this definition of Shape were substituted in lines 3–11 of Listing 18.3, it would become impossible to make an object of class Shape.

Implementing Pure Virtual Functions

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

The Shape2 program in Listing 18.4 defines Shape as an ADT and includes an implementation for the pure virtual function draw(). The Circle class overrides draw(), as it must, and then chains up to the base class function for additional functionality.

In this example, the additional functionality is simply an additional message displayed. A more robust graphical class could set up a shared drawing mechanism, perhaps setting up a window that all derived classes will use.

Listing 18.4 The Full Text of Shape2.cpp


  1: #include <iostream>
  2:
  3: class Shape
  4: {
  5: public:
  6:     Shape() {}
  7:     virtual ~Shape() {}
  8:     virtual long getArea() = 0;
  9:     virtual long getPerim()= 0;
 10:     virtual void draw() = 0;
 11: private:
 12: };
 13:
 14: void Shape::draw()
 15: {
 16:     std::cout << "Abstract drawing mechanism! ";
 17: }
 18:
 19: class Circle : public Shape
 20: {
 21: public:
 22:     Circle(int newRadius):radius(newRadius) {}
 23:     ~Circle() {}
 24:     long getArea() { return 3 * radius * radius; }
 25:     long getPerim() { return 9 * radius; }
 26:     void draw();
 27: private:
 28:     int radius;
 29:     int circumference;
 30: };
 31:
 32: void Circle::draw()
 33: {
 34:     std::cout << "Circle drawing routine here! ";
 35:     Shape::draw();
 36: }
 37:
 38: class Rectangle : public Shape
 39: {
 40: public:
 41:     Rectangle(int newLen, int newWidth):
 42:         length(newLen), width(newWidth) {}
 43:     virtual ~Rectangle() {}
 44:     long getArea() { return length * width; }
 45:     long getPerim() { return 2 * length + 2 * width; }
 46:     virtual int getLength() { return length; }
 47:     virtual int getWidth() { return width; }
 48:     void draw();
 49: private:
 50:     int width;
 51:     int length;
 52: };
 53:
 54: void Rectangle::draw()
 55: {
 56:     for (int i = 0; i < length; i++)
 57:     {
 58:         for (int j = 0; j < width; j++)
 59:             std::cout << "x ";
 60:
 61:         std::cout << " ";
 62:     }
 63:     Shape::draw();
 64: }
 65:
 66: class Square : public Rectangle
 67: {
 68: public:
 69:     Square(int len);
 70:     Square(int len, int width);
 71:     ~Square() {}
 72:     long getPerim() {return 4 * getLength();}
 73: };
 74:
 75: Square::Square(int newLen):
 76:     Rectangle(newLen, newLen)
 77: {}
 78:
 79: Square::Square(int newLen, int newWidth):
 80:     Rectangle(newLen, newWidth)
 81: {
 82:     if (getLength() != getWidth())
 83:         std::cout << "Error, not a square ... a rectangle? ";
 84: }
 85:
 86: int main()
 87: {
 88:     int choice;
 89:     bool fQuit = false;
 90:     Shape * sp;
 91:
 92:     while (1)
 93:     {
 94:         std::cout << "(1) Circle (2) Rectangle (3) Square (0) Quit: ";
 95:         std::cin >> choice;
 96:
 97:         switch (choice)
 98:         {
 99:         case 1:
100:             sp = new Circle(5);
101:             break;
102:         case 2:
103:             sp = new Rectangle(4, 6);
104:             break;
105:         case 3:
106:             sp = new Square(5);
107:             break;
108:         default:
109:             fQuit = true;
110:             break;
111:         }
112:         if (fQuit)
113:             break;
114:         sp->draw();
115:         std::cout << " ";
116:     }
117:     return 0;
118: }


Again, the user is asked which shapes to create. Here’s sample 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

On lines 3–12, the abstract data type Shape is declared, with all three of its accessors 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 member function, taking advantage of shared functionality in the base class.

Complex Hierarchies of Abstraction

At times, you will derive ADTs from other ADTs. It might be that you want to make some of the derived pure virtual functions nonpure and leave others pure.

If you create the Animal class, you can make eat(), sleep(), move(), and reproduce() pure virtual functions. Perhaps you derive Mammal and Fish from Animal.

On examination, you decide that every Mammal will reproduce in the same way, and so you make Mammal::reproduce() be nonpure, 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 say, as class designer, is that no Animal or Mammal objects can be instantiated, but that all Mammal objects can inherit the provided reproduce() method without overriding it.

The Animal class in Listing 18.5 illustrates this technique with a bare-bones implementation of these classes.

Listing 18.5 The Full Text of Animal.cpp


  1: #include <iostream>
  2:
  3: enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown } ;
  4:
  5: class Animal // common base to both horse and bird
  6: {
  7: public:
  8:     Animal(int);
  9:     virtual ~Animal() { std::cout << "Animal destructor ... "; }
 10:     virtual int getAge() const { return age; }
 11:     virtual void setAge(int newAge) { age = newAge; }
 12:     virtual void sleep() const = 0;
 13:     virtual void eat() const = 0;
 14:     virtual void reproduce() const = 0;
 15:     virtual void move() const = 0;
 16:     virtual void speak() const = 0;
 17: private:
 18:     int age;
 19: };
 20:
 21: Animal::Animal(int newAge):
 22: age(newAge)
 23: {
 24:     std::cout << "Animal constructor ... ";
 25: }
 26:
 27: class Mammal : public Animal
 28: {
 29: public:
 30:     Mammal(int newAge):Animal(newAge)
 31:         { std::cout << "Mammal constructor ... ";}
 32:     virtual ~Mammal() { std::cout << "Mammal destructor ... ";}
 33:     virtual void reproduce() const
 34:         { std::cout << "Mammal reproduction depicted ... "; }
 35: };
 36:
 37: class Fish : public Animal
 38: {
 39: public:
 40:     Fish(int newAge):Animal(newAge)
 41:         { std::cout << "Fish constructor ... ";}
 42:     virtual ~Fish()
 43:         { std::cout << "Fish destructor ... ";  }
 44:     virtual void sleep() const
 45:         { std::cout << "Fish snoring ... "; }
 46:     virtual void eat() const
 47:         { std::cout << "Fish feeding ... "; }
 48:     virtual void reproduce() const
 49:         { std::cout << "Fish laying eggs ... "; }
 50:     virtual void move() const
 51:         { std::cout << "Fish swimming ... ";   }
 52:     virtual void speak() const { }
 53: };
 54:
 55: class Horse : public Mammal
 56: {
 57: public:
 58:     Horse(int newAge, COLOR newColor):
 59:         Mammal(newAge), color(newColor)
 60:         { std::cout << "Horse constructor ... "; }
 61:     virtual ~Horse()
 62:         { std::cout << "Horse destructor ... "; }
 63:     virtual void speak() const
 64:         { std::cout << "Whinny! "; }
 65:     virtual COLOR getcolor() const
 66:         { return color; }
 67:     virtual void sleep() const
 68:         { std::cout << "Horse snoring ... "; }
 69:     virtual void eat() const
 70:         { std::cout << "Horse feeding ... "; }
 71:     virtual void move() const
 72:         { std::cout << "Horse running ... ";}
 73:
 74: protected:
 75:     COLOR color;
 76: };
 77:
 78: class Dog : public Mammal
 79: {
 80: public:
 81:     Dog(int newAge, COLOR newColor ):
 82:         Mammal(newAge), color(newColor)
 83:         { std::cout << "Dog constructor ... "; }
 84:     virtual ~Dog()
 85:         { std::cout << "Dog destructor ... "; }
 86:     virtual void speak() const
 87:         { std::cout << "Whoof! "; }
 88:     virtual void sleep() const
 89:         { std::cout << "Dog snoring ... "; }
 90:     virtual void eat() const
 91:         { std::cout << "Dog eating ... "; }
 92:     virtual void move() const
 93:         { std::cout << "Dog running... "; }
 94:     virtual void reproduce() const
 95:         { std::cout << "Dogs reproducing ... "; }
 96:
 97: protected:
 98:     COLOR color;
 99: };
100:
101: int main()
102: {
103:     Animal *pAnimal = 0;
104:     int choice;
105:     bool fQuit = false;
106:
107:     while (1)
108:     {
109:         std::cout << "(1) Dog (2) Horse (3) Fish (0) Quit: ";
110:         std::cin >> choice;
111:
112:         switch (choice)
113:         {
114:         case 1:
115:             pAnimal = new Dog(5, Brown);
116:             break;
117:         case 2:
118:             pAnimal = new Horse(4, Black);
119:             break;
120:         case 3:
121:             pAnimal = new Fish(5);
122:             break;
123:         default:
124:             fQuit = true;
125:             break;
126:         }
127:         if (fQuit)
128:             break;
129:
130:         pAnimal->speak();
131:         pAnimal->eat();
132:         pAnimal->reproduce();
133:         pAnimal->move();
134:         pAnimal->sleep();
135:         delete pAnimal;
136:         std::cout << " ";
137:     }
138:     return 0;
139: }


Here’s sample output from a run of this program:

(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

On lines 5–19, the abstract data type Animal is declared. Animal has nonpure virtual accessors for age, which are shared by all Animal objects. It has five pure virtual functions: sleep(), eat(), reproduce(), move(), and speak().

Mammal derives from Animal and is declared on lines 27–35. It adds no data. It overrides reproduce(), however, providing a common form of reproduction for all Mammal objects. Fish must override reproduce() because Fish derives directly from Animal and cannot take advantage of Mammal reproduction.

Mammal classes no longer have to override the reproduce() function, but they are free to do so if they choose (as Dog does on lines 94–95). 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 member functions are invoked, and, based on the runtime binding of the pointer, the correct function is called in the derived class.

It would cause a compile-time error to try to instantiate an Animal or a Mammal, because both are abstract data types.

Which Types Are Abstract?

In one program, the class Animal is abstract; in another it is not. What determines whether to make a class abstract?

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 might 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 might want to keep Dog as an abstract data type and only instantiate types of dogs: retrievers, terriers, and so forth. The level of abstraction is a function of how finely you need to distinguish your types.

Summary

During this hour, you learned about abstract data types and pure virtual functions, two aspects of the C++ language that make its support for object-oriented programming more robust.

An abstract data type is a class that cannot be implemented as an object. Instead, it defines common member variables and functions for its derived classes.

A function becomes a pure virtual function by adding = 0 to the end of its declaration. If a class contains at least one pure function, the class is an abstract data type.

The compiler will not allow objects of an abstract data type to be instantiated.

Q&A

Q. What does percolating functionality upward 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 upward always a good thing?

A. Yes, if you are percolating shared functionality upward; no, if all you are moving is an interface. If all the derived classes can’t use the member function, it is a mistake to move it up into a common base class. If you do, you must check the runtime type of the object before deciding whether you can invoke the function.

Q. Why is dynamic casting bad?

A. The point of virtual functions is to let the virtual table, rather than the programmer, determine the runtime type of the object.

Q. Why bother making an abstract data type? Why not just make it nonabstract 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 to avoid runtime bugs in completed code. Making a class abstract by giving it pure virtual functions causes the compiler to flag any objects created of that abstract type as errors. It also means that you can share the abstract data types with other applications or programmers.

Q. Has anyone ever studied Silbo, the whistling language used on Gomera in the Canary Islands?

A. The best reference on the topic of Silbo Gomero and similar forms of speech is Whistled Languages, a 1976 book by R. G. Busnel and A. Classe that’s available at many public libraries.

Silbo allows shepherds to communicate over deep ravines and narrow valleys at distances of 3 miles or more. The language has fallen into disuse on the island of 21,000.

The disappearance of rural society and the introduction of the telephone reduced the Gomeran whistle to being used primarily in demonstrations (although these days schoolchildren are being taught it).

A speaker of Silbo Gomero is referred to in Spanish as a silbador.

Workshop

You’ve just learned more about polymorphism and should answer a few questions and do a couple of exercises to firm up your knowledge.

Quiz

1. Is there a difference between a virtual member function and an overridden member function in the base class?

A. Yes

B. No

C. Reply hazy, try again

2. What makes an abstract data type abstract?

A. The abstract keyword

B. It has at least one pure virtual function.

C. It has all pure virtual functions.

3. Is it okay to supply base class member functions that apply only to specific derived classes?

A. Yes

B. No

C. It varies.

Answers

1. A. A virtual function expects to be overridden in the derived classes. An overridden function does not have to be in derived classes.

2. B. The class is abstract because it has at least one pure virtual function that cannot be called directly. Because of that function, you cannot create a concrete example of that class and must derive a class from the abstract with the proper methods and instantiate from the derived class.

3. B. Base class member functions that apply only to some classes make it much harder to use derived objects. The function only can be called after checking the data type of each object.

Activities

1. Modify the Animal program in Listing 18.5 to instantiate an object of Animal or Mammal type. What does the compiler tell you and why?

2. Modify the Mammal13 program in Listing 18.2 to see what happens if you remove the if test in lines 55—58 and call the purr() member function in all cases. Which objects work properly and which fail?

To see solutions to these activities, visit this 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
18.191.239.178