15.3 Polymorphism

All experience is an arch, to build upon.

HENRY ADAMS, The Education of Henry Adams

Polymorphism refers to the ability to associate multiple meanings to one function name. As it has come to be used today, polymorphism refers to a very particular way of associating multiple meanings to a single function name. That is, polymorphism refers to the ability to associate multiple meanings to one function name by means of a special mechanism known as late binding. Polymorphism is one of the key components of a programming philosophy known as object-oriented programming. Late binding, and therefore polymorphism, is the topic of this section.

Late Binding

A virtual function is one that, in some sense, may be used before it is defined. For example, a graphics program may have several kinds of figures, such as rectangles, circles, ovals, and so forth. Each figure might be an object of a different class. For example, the Rectangle class might have member variables for a height, width, and center point, while the Circle class might have member variables for a center point and a radius. In a well-designed programming project, all of them would probably be descendants of a single parent class called, for example, Figure. Now, suppose you want a function to draw a figure on the screen. To draw a circle, you need different instructions from those you need to draw a rectangle. So, each class needs to have a different function to draw its kind of figure. However, because the functions belong to the classes, they can all be called draw. If r is a Rectangle object and c is a Circle object, then r.draw ( ) and c.draw ( ) can be functions implemented with different code. All this is not news, but now we move on to something new: virtual functions defined in the parent class Figure.

Now, the parent class Figure may have functions that apply to all figures. For example, it might have a function called center that moves a figure to the center of the screen by erasing it and then redrawing it in the center of the screen. Figure::center might use the function draw to redraw the figure in the center of the screen. When you think of using the inherited function center with figures of the classes Rectangle and Circle, you begin to see that there are complications here.

To make the point clear and more dramatic, let’s suppose the class Figure is already written and in use and at some later time we add a class for a brand-new kind of figure, say, the class Triangle. Now, Triangle can be a derived class of the class Figure, and so the function center will be inherited from the class Figure; thus, the function center should apply to (and perform correctly for!) all Triangles. But there is a complication. The function center uses draw, and the function draw is different for each type of figure. The inherited function center (if nothing special is done) will use the definition of the function draw given in the class Figure, and that function draw does not work correctly for Triangles. We want the inherited function center to use the function Triangle::draw rather than the function Figure::draw. But the class Triangle, and therefore the function Triangle::draw, was not even written when the function center (defined in the class Figure) was written and compiled! How can the function center possibly work correctly for Triangles? The compiler did not know anything about Triangle::draw at the time that center was compiled. The answer is that it can apply provided draw is a virtual function.

When you make a function virtual, you are telling the compiler, “I do not know how this function is implemented. Wait until it is used in a program, and then get the implementation from the object instance.” The technique of waiting until run-time to determine the implementation of a procedure is called late binding or dynamic binding. Virtual functions are the way C++ provides late binding. But enough introduction. We need an example to make this come alive (and to teach you how to use virtual functions in your programs). In order to explain the details of virtual functions in C++, we will use a simplified example from an application area other than drawing figures.

Virtual Functions in C++

Suppose you are designing a record-keeping program for an automobile parts store. You want to make the program versatile, but you are not sure you can account for all possible situations. For example, you want to keep track of sales, but you cannot anticipate all types of sales. At first, there will be only regular sales to retail customers who go to the store to buy one particular part. However, later you may want to add sales with discounts, or mail-order sales with a shipping charge. All these sales will be for an item with a basic price and ultimately will produce some bill. For a simple sale, the bill is just the basic price, but if you later add discounts, then some kinds of bills will also depend on the size of the discount. Your program will need to compute daily gross sales, which intuitively should just be the sum of all the individual sales bills. You may also want to calculate the largest and smallest sales of the day or the average sale for the day. All these can be calculated from the individual bills, but the functions for computing the bills will not be added until later, when you decide what types of sales you will be dealing with. To accommodate this, we make the function for computing the bill a virtual function. (For simplicity in this first example, we assume that each sale is for just one item, although with derived classes and virtual functions we could, but will not here, account for sales of multiple items.)

Displays 15.9 and 15.10 contain the interface and implementation for the class Sale. All types of sales will be derived classes of the class Sale. The class Sale corresponds to simple sales of a single item with no added discounts or charges. Notice the reserved word virtual in the function declaration for the function bill (Display 15.9). Notice (Display 15.10) that the member function savings and the overloaded operator < both use the function bill. Since bill is declared to be a virtual function, we can later define derived classes of the class Sale and define their versions of the function bill, and the definitions of the member function savings and the overloaded operator <, which we gave with the class Sale, will use the version of the function bill that corresponds to the object of the derived class.

Display 15.9 Interface for the Base Class Sale

 1    //This is the header file sale.h.
 2    //This is the interface for the class Sale.
 3    //Sale is a class for simple sales.
 4    #ifndef SALE_H
 5    #define SALE_H
 6     
 7    #include <iostream>
 8    using namespace std;
 9     
10    namespace salesavitch
11    {
12     
13        class Sale
14        {
15        public:
16            Sale();
17            Sale(double thePrice);
18            virtual double  bill() const;
19            double savings(const Sale& other) const;
20            //Returns the savings if you buy other instead of the calling object.
21        protected:
22                double price;
23        };
24     
25        bool operator <(const Sale& first, const Sale& second);
26        //Compares two sales to see which is larger.
27    }//salesavitch
28     
29    #endif // SALE_H

Display 15.10 Implementation of the Base Class Sale

 1    //This is the implementation file: sale.cpp
 2    //This is the implementation for the class Sale.
 3    //The interface for the class Sale is in
 4    //the header file sale.h.
 5    #include "sale.h"
 6     
 7    namespace salesavitch
 8    {
 9        Sale::Sale() : price(0)
10        {}
11         
12        Sale::Sale(double thePrice) : price(thePrice)
13        {}
14          
15        double Sale::bill() const
16        {
17            return price;
18        }
19         
20        double Sale::savings(const Sale& other) const
21        {
22            return ( bill() − other.bill() );
23        }
24         
25        bool operator <(const Sale& first, const Sale& second)
26        {
27            return (first.bill() < second.bill());
28        }
29    }//salesavitch

For example, Display 15.11 shows the derived class DiscountSale. Notice that the class DiscountSale requires a different definition for its version of the function bill. Nonetheless, when the member function savings and the overloaded operator < are used with an object of the class DiscountSale, they will use the version of the function definition for bill that was given with the class DiscountSale. This is indeed a pretty fancy trick for C++ to pull off. Consider the function call d1.savings(d2) for objects d1 and d2 of the class DiscountSale. The definition of the function savings (even for an object of the class DiscountSale) is given in the implementation file for the base class Sale, which was compiled before we ever even thought of the class DiscountSale. Yet, in the function call d1.savings(d2), the line that calls the function bill knows enough to use the definition of the function bill given for the class DiscountSale.

Display 15.11 The Derived Class DiscountSale

An illustration shows a code segment with a derived class “DiscountSale.”

How does this work? In order to write C++ programs, you can just assume it happens by magic, but the real explanation was given in the introduction to this section. When you label a function virtual, you are telling the C++ environment, “Wait until this function is used in a program, and then get the implementation corresponding to the calling object.”

Display 15.12 gives a sample program that illustrates how the virtual function bill and the functions that use bill work in a complete program.

Display 15.12 Use of a Virtual Function

 1    //Demonstrates the performance of the virtual function bill.
 2    #include <iostream>
 3    #include "sale.h" //Not really needed, but safe due to ifndef.
 4    #include "discountsale.h"
 5    using namespace std;
 6    using namespace salesavitch;
 7     
 8    int main()
 9    {
10        Sale simple(10.00); //One item at $10.00.
11        DiscountSale discount(11.00, 10); //One item at $11.00 at 10% discount.     
12         
13        cout.setf(ios::fixed);
14        cout.setf(ios::showpoint);
15        cout.precision(2);
16        
17        if (discount < simple)
18        {
19            cout << "Discounted item is cheaper.\n";
20            cout << "Savings is $" << simple.savings(discount) << endl;
21        }
22        else
23            cout << "Discounted item is not cheaper.\n";
24         
25        return 0;
26    }

Sample Dialogue

Discounted item is cheaper.
Savings is $0.10

There are a number of technical details you need to know in order to use virtual functions in C++. We list them here:

Since virtual functions are so great, why not make all member functions virtual? Almost the only reason for not always using virtual functions is efficiency. The compiler and the run-time environment need to do much more work for virtual functions, and so if you label more member functions virtual than you need to, your programs will be less efficient.

Self-Test Exercise

  1. Suppose you modify the definitions of the class Sale (Display 15.9) by deleting the reserved word virtual. How would that change the output of the program in Display 15.12?

Virtual Functions and Extended Type Compatibility

We will discuss some of the further consequences of declaring a class member function to be virtual and do one example that uses some of these features.

C++ is a fairly strongly typed language. This means that the types of items are always checked and an error message is issued if there is a type mismatch, such as a type mismatch between an argument and a formal parameter when there is no conversion that can be automatically invoked. This also means that normally the value assigned to a variable must match the type of the variable, although in a few well-defined cases C++ will perform an automatic type cast (called a coercion) so that it appears that you can assign a value of one type to a variable of another type. For example, C++ allows you to assign a value of type char or int to a variable of type double. However, C++ does not allow you to assign a value of type double or float to a variable of any integer type (char, short, int, long).

However, as important as strong typing is, this strong type checking interferes with the very idea of inheritance in object-oriented programming. Suppose you have defined class A and class B and have defined objects of type class A and class B. You cannot always assign between objects of these types. For example, suppose a program or unit contains the following type declarations:

class Pet
{ 
public:
     virtual void print();
     string name;
};
 class Dog : public Pet
{
 public:
     virtual void print(); //Keyword virtual not needed, but is
             //put here for clarity. (It is also good style!)
     string breed;
};
Dog vDog;
Pet vPet;

Now concentrate on the data members, name and breed. (To keep this example simple, we have made the member variables public. In a real application, they should be private and have functions to manipulate them.)

Anything that is a Dog is also a Pet. It would seem to make sense to allow programs to consider values of type Dog to also be values of type Pet, and hence the following should be allowed:

vDog.name = "Tiny";
vDog.breed = "Great Dane";
vPet = vDog;

C++ does allow this sort of assignment. You may assign a value, such as the value of vDog, to a variable of a parent type, such as vPet, but you are not allowed to perform the reverse assignment. Although the assignment above is allowed, the value that is assigned to the variable vPet loses its breed field. This is called the slicing problem. The following attempted access will produce an error message:

cout << vPet.breed; //Illegal: class Pet has no member named breed

You can argue that this makes sense, since once a Dog is moved to a variable of type Pet it should be treated like any other Pet and not have properties peculiar to Dogs. This makes for a lively philosophical debate, but it usually just makes for a nuisance when programming. The dog named Tiny is still a Great Dane and we would like to refer to its breed, even if we treated it as a Pet someplace along the line.

Fortunately, C++ does offer us a way to treat a Dog as a Pet without throwing away the name of the breed. To do this, we use pointers to dynamic object instances. Suppose we add the following declarations:

Pet *pPet;
Dog *pDog;

If we use pointers and dynamic variables, we can treat Tiny as a Pet without losing his breed. The following is allowed:

pDog = new Dog;
pDog->name = "Tiny";
pDog->breed = "Great Dane";
pPet = pDog;

Moreover, we can still access the breed field of the node pointed to by pPet. Suppose that

Dog::print();

has been defined as follows:

//uses iostream
void Dog::print()
{
    cout << "name: " << name << endl;
    cout << "breed: " << breed << endl;
}

The statement

pPet->print();

will cause the following to be printed on the screen:

name: Tiny
breed: Great Dane

This is by virtue of the fact that print() is a virtual member function. (No pun intended.) We have included test code in Display 15.13.

Display 15.13 More Inheritance with Virtual Functions

 1    //Program to illustrate use of a virtual function
 2    //to defeat the slicing problem.

 3    #include <string>
 4    #include <iostream>
 5    using namespace std;
 6     
 7    class Pet
 8    {
 9    public:
10        virtual void print();
11        string name;
12    };
13     
14     class Dog : public Pet
15    {
16    public:
17        virtual void print(); //Keyword virtual not needed, but put
18                              //here for clarity. (It is also good style!)
19        string breed;
20    };
21     
22    int main()
23    {
24        Dog vDog;
25        Pet vPet;
26     
27        vDog.name = "Tiny";
28        vDog.breed = "Great Dane";
29        vPet = vdog;
30         
31        //vPet.breed; is illegal since class Pet has no member named breed
32         
33        Dog *pDog;
34        pDog = new Dog;
35        pDog->name = "Tiny";
36        pDog->breed = "Great Dane";
37         
38        Pet *pPet;
39        pPet = pDog;
40        pPet->print(); // These two print the same output:
41        pDog->print(); // name: Tiny breed: Great Dane 42         
43        //The following, which accesses member variables directly
44        //rather than via virtual functions, would produce an error:
45        //cout << "name: " << pPet->name << "  breed: "
46        //     << pPet->breed << endl;
47        //generates an error message: 'class Pet' has no member
48        //named 'breed' .
49        //See Pitfall section "Not Using Virtual Member Functions"
50        //for more discussion on this.
51         
52        return 0;
53    }
54     
55    void Dog::print()
56    {
57        cout << "name: " << name << endl;
58        cout << "breed: " << breed << endl;
59    }
60     
61    void Pet::print()
62    {
63        cout << "name: " << endl;//Note no breed mentioned
64    }

Sample Dialogue

name: Tiny
breed: Great Dane
name: Tiny
breed: Great Dane

Programming Tip Make Destructors Virtual

It is a good policy to always make destructors virtual, but before we explain why this is a good policy, we need to say a word or two about how destructors and pointers interact and about what it means for a destructor to be virtual.

Consider the following code, where SomeClass is a class with a destructor that is not virtual:

SomeClass *p = new SomeClass;
    . . . delete p;

When delete is invoked with p, the destructor of the class SomeClass is automatically invoked. Now, let’s see what happens when a destructor is marked as virtual.

The easiest way to describe how destructors interact with the virtual function mechanism is that destructors are treated as if all destructors had the same name (even though they do not really have the same name). For example, suppose Derived is a derived class of the class Base and suppose the destructor in the class Base is marked virtual. Now consider the following code:

Base *pBase = new Derived;
    . . . delete pBase;

When delete is invoked with pBase, a destructor is called. Since the destructor in the class Base was marked virtual and the object pointed to is of type Derived, the destructor for the class Derived is called (and it in turn calls the destructor for the class Base). If the destructor in the class Base had not been declared as virtual, then only the destructor in the class Base would be called.

Another point to keep in mind is that when a destructor is marked as virtual, then all destructors of derived classes are automatically virtual (whether or not they are marked virtual). Again, this behavior is as if all destructors had the same name (even though they do not).

Now we are ready to explain why all destructors should be virtual. Suppose the class Base has a member variable pB of a pointer type, the constructor for the class Base creates a dynamic variable pointed to by pB, and the destructor for the class Base deletes the dynamic variable pointed to by pB. And suppose the destructor for the class Base is not marked virtual. Also suppose that the class Derived (which is derived from Base) has a member variable pD of a pointer type, the constructor for the class Derived creates a dynamic variable pointed to by pD, and the destructor for the class Derived deletes the dynamic variable pointed to by pD. Consider the following code:

Base *pBase = new Derived;
. . .
delete pBase;

Since the destructor in the base class is not marked virtual, only the destructor for the class Base will be invoked. This will return to the freestore the memory for the dynamic variable pointed to by pB, but the memory for the dynamic variable pointed to by pD will never be returned to the freestore (until the program ends).

On the other hand, if the destructor for the base class Base were marked virtual, then when delete is applied to pBase, the destructor for the class Derived would be invoked (since the object pointed to is of type Derived). The destructor for the class Derive would delete the dynamic variable pointed to by pD and then automatically invoke the destructor for the base class Base, and that would delete the dynamic variable pointed to by pB. So, with the base class destructor marked as virtual, all the memory is returned to the freestore. To prepare for eventualities such as these, it is best to always mark destructors as virtual.

Self-Test Exercises

  1. Why can’t we assign a base class object to a derived class variable?

  2. What is the problem with the (legal) assignment of a derived class object to a base class variable?

  3. Suppose the base class and the derived class each have a member function with the same signature. When you have a pointer to a base class object and call a function member through the pointer, discuss what determines which function is actually called—the base class member function or the derived-class function.