Polymorphism is the third and final characteristic that defines the object-oriented paradigm. Admittedly, polymorphism doesn't allow us to solve more problems, but it does allow us to solve them 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 subclasses of this new class. 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 in place, making a function virtual
in the superclass switches on or enables polymorphism. Note that once a function is made virtual
in the superclass (Shape), all overridden versions of the function in the subclasses are automatically virtual
, making the "virtual" 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 previous version required three separate variables, one for each kind of shape that the program can create and draw. Furthermore, if we added a new shape, an Ellipse, for example, the new shape would require adding 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.
The most profound changes occur in the third block of code, where the shape is dawn. The previous version required a switch statement to determine which shape the user created so the program could call the appropriate draw function (see Drawing the shape). This version dispenses with the switch statement and replaces it with a polymorphic function call.
Comparing Figure 4 above to Figure 3 in the original shape drawing problem gives a sense of polymorphism's computational power and potential. The two code fragments do the same thing, drawing the user's selected shape. The polymorphic version above does not bind to the correct draw function until the time of the function call. By the time the program makes the polymorphic function call, the user has selected which shape to draw, and the program has instantiated the appropriate object. The object determines which function to call: the program "sends the draw message to the object, and it responds appropriately depending on what kind of object it is."
Polymorphism
Requirements for polymorphism:
virtual
Assignment | Function Call |
---|---|
Shape* s = new Circle(...); |
void render(Shape* s) {...} render(new Circle(...)); |
‡ The function override is necessary because the compiler always begins searching in the symbol table with the pointer-variable class type. In this example, the compiler enters the symbol table at the Shape entry, and compilation fails if it doesn't find a draw function. Once it finds a virtual
function, draw for example, polymorphism takes over.
Non-polymorphic | Polymorphic |
---|---|
s->draw(); |
s->draw(); |
Let's return to the drawing problem introduced at the beginning of the chapter. With polymorphism in place, what must we do if the user wishes to add an Ellipse shape?
virtual
keyword optional.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 - its public member functions. For example, Figure 7 illustrates different kinds of employees with different member variables and ways of calculating their pay. If 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. The SalesEmployee calc_pay() function can call the SalariedEmployee calc_pay() function and add the commission. This scheme works well until we introduce an upcast.
If we upcast a SalesEmployee object to SalariedEmployee, it won't be able to access commission without a downcast. Without saying why we may want to do the upcast, let's note that we can achieve an elegant solution by making calc_pay() polymorphic. Compare the requirements for polymorphism to Figures 7 and 8, and verify that all polymorphism requirements are satisfied.
SalariedEmployee | SalesEmployee |
---|---|
virtual double calc_pay() { return salary / 24; } |
virtual double calc_pay() { return SalariedEmployee::calc_pay() + commission; } |
(a) | (b) |
Previously, we saw that an an upcast followed by an incorrect downcast was potentially meaningless and dangerous. Although polymorphism greatly reduces the need to downcast to access the member variables in an object, it is sometimes necessary. In these cases, polymorphism provides a way to ensure that the downcast is meaningful and safe. For example, suppose that sometimes, after an object is selected and instantiated (Figure 3 above), we need 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 do we know to which shape to safely downcast s? Polymorphism gives us two different ways of solving this 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 in order 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. This code will compile even when the classes do not have a polymorphic function, but it will fail to choose 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
.