Polymorphism is the third and final characteristic that defines the object-oriented paradigm. Admittedly, any problem we can solve with polymorphism, we can solve without it. However, the polymorphic solution will always be more elegant, compact, and easier to follow - once you understand polymorphism. So, if the following discussion seems confusing at times, 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 in comparison.
A key concept to keep in mind 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 of 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) that 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 - the code where the shape is dawn. The previous version required a switch statement to determine which shape the user created and to then call the appropriate draw function (see Drawing the shape). This version dispenses with the switch statement and replaces it with a polymorphic function call.
You really must compare Figure 4 above to Figure 3 in the original shape drawing problem. The two code fragments do the same thing! They both draw the user's selected shape. The polymorphic version above does not bind to the correct draw function until the time of the function call. At that time, it is the object to which the pointer points that determines which function is called.
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 do we need to 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 is substantially different than 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, then their pay is salary / 24
. A sales employee is a salaried employee by inheritance. So, sales employees earn a salary, paid twice per month, plus a commission. Logically, a sales employee's pay is salary / 24 + commission
.
But 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) that 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 that dynamic_cast<Circle*>(s)
is identical in behavior to (Circle *)s
.