Polymorphism is the third and final characteristic that defines the object-oriented paradigm. Admittedly, polymorphism doesn't solve more problems, but it solves some more compactly, elegantly, and clearly. If the following discussion seems confusing, remember that any alternative solution will involve either maintaining numerous variables, passing them as function arguments, duplicating large amounts of code, or creating, maintaining, passing, and using arrays of function pointers. Polymorphism is simple by comparison.
A key concept to remember throughout the following discussion is that the compiler uses different algorithms to locate polymorphic functions than it uses to find member variables and non-polymorphic functions. In the latter case, the compiler creates simple expressions. But polymorphism requires pointers, tables, and address arithmetic.
Programmers say that "normal" (i.e., non-polymorphic) functions have compile time binding, early binding, or static binding. These terms suggest that the compiler system can connect a function call to the function's machine instructions when programmers compile and link the program. Polymorphism delays function binding from compile-time to run-time. Computer scientists use several different terms to denote this kind of binding:
The first and most obvious change that we must make to the previous problem is to introduce a new superclass named Shape and to make the original classes its subclasses. Figure 1 illustrates the UML class diagram that describes the class hierarchy for the restated problem:
Finally, we have all of the pieces in place to make the final step from a member function to a polymorphic function - that is, we are ready to switch on polymorphism, which we do by adding the virtual keyword to a function's prototype. The code appearing in Figure 2 illustrates making "virtual" functions and building an inheritance relationship.
class Shape
{
public:
virtual void draw();
};
|
class Circle : public Shape
{
public:
virtual void draw();
};
|
class Rectangle : public Shape
{
public:
virtual void draw();
};
|
class Triangle : public Shape
{
public:
virtual void draw();
};
|
virtual keyword is the last ingredient in the polymorphic recipe. Once all the other requirements are met, making a function virtual in the superclass enables polymorphism. When a subclass overrides a virtual superclass function, the subclass function inherits its "virtualness," making the keyword optional in the subclasses (Circle, Rectangle, and Triangle).
The introduction of the Shape class, along with upcasting, allows us to simplify the code from the previous version of the shape drawing problem. The last version required three separate variables - one for each kind of shape the program can create and draw. Furthermore, if we added a new shape, such as an Ellipse, we would need to add a new variable to the program. Using inheritance and upcasting, we can have a Shape pointer variable that can point to any shape, including shapes that we create in the future.
cout << "C:\tCircle" << endl; cout << "S:\tRectangle" << endl; cout << "T:\tTriangle" << endl; cout << "Please choose a shape: "; char choice; cin >> choice; cin.ignore();Circle* c; Rectangle* r; Triangle* t;Shape* s = nullptr; switch (choice) { case 'C' : case 'c' : s = new Circle(); break; case 'S' : case 's' : s = new Rectangle(); break; case 'T' : case 't' : s = new Triangle(); break; }
The most profound changes occur in the block of code drawing the shape, which requires a switch statement to determine which shape the user created so the program can call the appropriate draw function. This version dispenses with the switch and replaces it with a polymorphic function call.
| Without Polymorphism | With Polymorphism |
|---|---|
switch (choice)
{
case 'C' :
case 'c' :
c->draw();
break;
case 'R' :
case 'r' :
r->draw();
break;
case 'T' :
case 't' :
t->draw();
break;
} |
if (s != nullptr) s->draw(); |
Comparing the two versions illustrates polymorphism's computational power and flexibility. The two code fragments do the same thing: they draw the user's selected shape. However, they bind the shape objects (c, r, t, and s) to the draw function at different times: compile-time versus run-time. The statement s->draw() polymorphically selects the correct draw function and binds it to object s when the statement executes. Alternatively, we can say that the statement "sends the draw message to object s, and s responds appropriately depending on the object it points to."
Returning to the drawing problem introduced at the beginning of the chapter, with polymorphism in place, what steps must a programmer take to add an Ellipse shape to the program?
The polymorphic solution has the same three steps as our initial solution, so it may not seem like much of an improvement. But step 3 of the polymorphic solution differs substantially from the original. An actual program typically uses objects in many locations throughout the code - potentially spread across thousands of lines of code and hundreds of files. Without polymorphism, programmers must find and update every place the program uses the object, and the odds of missing one of these are quite high. Alternatively, the polymorphic solution doesn't require any changes.
When programming with objects, it's common to "hide" variables by making them private class members. Programs access private variables through the class's public interface. For example, the employee inheritance hierarchy illustrates three types of employees with distinct member variables and methods for calculating their pay. Assuming that salaried employees are paid twice a month, their pay is salary / 24. A sales employee is a salaried employee by inheritance, earning a salary plus a commission. Logically, a sales employee's pay is salary / 24 + commission.
However, salary is private in the SalariedEmployee class, so a SalesEmployee object cannot access it. The proper object-oriented solution is to call a member function in the superclass. All three classes define a public member function named calc_pay() that calculates the pay for a specific kind of employee. Therefore, the SalesEmployee calc_pay() function can call the SalariedEmployee calc_pay() function and add the commission. Without polymorphism, this scheme works well until we introduce an upcast.
If the program upcasts a SalesEmployee object to SalariedEmployee (perhaps by passing it as a function argument), it can't access the commission without a downcast. However, making calc_pay() polymorphic provide an elegant solution.
| SalariedEmployee | SalesEmployee |
|---|---|
virtual double calc_pay()
{
return salary / 24;
}
|
virtual double calc_pay()
{
return SalariedEmployee::calc_pay() + commission;
}
|
| (a) | (b) |
The previous illustrated that an upcast followed by an incorrect downcast was potentially meaningless and dangerous. Although polymorphism greatly reduces the need to downcast to access an object's member variables, it is sometimes necessary to do so. In these cases, polymorphism provides a way to ensure that downcasts are meaningful and safe. Imagine that after an object is selected and instantiated (Figure 3), the program needs to downcast pointer s to a specific shape class. If the user selected a Circle, we don't want to downcast s to a Rectangle or a Triangle. How can the program determine if a specific downcast is safe and meaningful? Polymorphism provides two ways of solving the problem:
#include <iostream>
#include <typeinfo>
using namespace std;
int main()
{
Shape* s = new Circle;
Circle* c;
Rectangle* r;
Triangle* t;
if (typeid(*s) == typeid(Circle))
c = (Circle *)s;
else if (typeid(*s) == typeid(Rectangle))
r = (Rectangle *)s;
else if (typeid(*s) == typeid(Triangle))
t = (Triangle *)s;
return 0;
} |
#include <iostream>
using namespace std;
int main()
{
Shape* s = new Circle;
Circle* c;
Rectangle* r;
Triangle* t;
if (dynamic_cast<Circle*>(s) != nullptr)
c = dynamic_cast<Circle*>(s);
else if (dynamic_cast<Rectangle*>(s) != nullptr)
r = dynamic_cast<Rectangle*>(s);
else if (dynamic_cast<Triangle*>(s) != nullptr)
t = dynamic_cast<Triangle*>(s);
return 0;
}
|
| (a) | (b) |
typeid and dynamic_cast require that the classes have at least one virtual function to identify a safe downcast.
typeid function returns an instance of type_info (similar to an instance of Java's Class class). So
typeid(*s) returns a type_info object that describes s (the * is the dereference operator)typeid(Circle) returns a type_info object that describes the Circle classtype_info objects are the same, s points to a Circle object, and the downcast is safe. Although the code compiles when the classes lack polymorphic functions, it fails to select a safe downcast (see the downloadable code).dynamic_cast represents an attempt to cast the pointer to the class named in angle brackets. The cast returns nullptr if it fails (indicating an unsafe downcast) or non-nullptr if it succeeds (indicating a safe downcast). Note that when performing an actual downcast operation, dynamic_cast<Circle*>(s) is identical in behavior to (Circle *)s.Understanding how code behaves, including polymorphic code, is an essential skill every software practitioner must develop. They develop and deepen their understanding through experience and extended practice. Used as a checklist, the polymorphism requirements are a helpful starting point for distinguishing between polymorphic and non-polymorphic code. The following figure restates the requirements and describes a stopgap algorithm for identifying polymorphic code, laying the foundation for a self-test exercise.
Polymorphism
Requirements for polymorphism:
virtual function, draw for example, polymorphism takes over.virtual.| Assignment | Function Call |
|---|---|
Shape* s = new Circle(...); |
void render(Shape* s) {...} render(new Circle(...)); |
| Non-polymorphic | Polymorphic |
|---|---|
s->draw(); |
s->draw(); |
Use the checklist to answer the questions in the following self-test.
| View | Download | Comments |
|---|---|---|
| shape-non-poly.cpp | shape-non-poly.cpp | The non-polymorphic version of the shape demonstration program from the first section |
| shape-poly.cpp | shape-poly.cpp | The complete polymorphic version of the shape demonstration program |
| cast.cpp | cast.cpp | Safe downcasting example demonstrating typeid and dynamic_cast |