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.
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 Triangle
s. 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 Triangle
s. 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 Triangle
s? 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.
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.
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
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
.
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.
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:
If a function will have a different definition in a derived class than in the base class and you want it to be a virtual function, you add the keyword virtual
to the function declaration in the base class. You do not need to add the reserved word virtual
to the function declaration in the derived class. If a function is virtual in the base class, then it is automatically virtual in the derived class. (However, it is a good idea to label the function declaration in the derived class virtual
, even though it is not required.)
The reserved word virtual
is added to the function declaration and not to the function definition.
You do not get a virtual function and the benefits of virtual functions unless you use the keyword virtual
.
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.
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?
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 Dog
s. 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.
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
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.
Why can’t we assign a base class object to a derived class variable?
What is the problem with the (legal) assignment of a derived class object to a base class variable?
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.