12.3. Polymorphism In Depth

polymorphism
Time: 00:11:46 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides: PDF, PPTX
Review

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.

Function Binding, Part 2

polymorphism, function binding, run-time binding, dynamic binding, late binding, dynamic dispatch

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:

These terms mean only that the program delays function binding (i.e., the decision of which function to call) until it runs. To see how we enable polymorphism, we return to the problem of drawing shapes without polymorphism, but this time polymorphism replaces the switch statement to select the correct drawing function.

Drawing Shapes With Polymorphism

polymorphism, Shape class, Circle class, Rectangle class, Triangle class, virtual

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:

The UML Shape class diagram or hierarchy consists of four classes: Shape, Circle, Rectangle, and Triangle. None of the classes show member variables.

Shape
-----------------
-----------------
+draw() : void

Circle
-----------------
-----------------
+draw() : void

Rectangle
-----------------
-----------------
+draw() : void

Triangle
-----------------
-----------------
+draw() : void
UML class diagram of the Shape classes. Polymorphism requires inheritance and an overridden function. Shape is the superclass or parent class; Circle, Rectangle, and Triangle are all subclasses or children of Shape. The draw functions in the subclasses override the draw function in the Shape class.

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();
};
Making functions virtual. The 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;
}
Instantiating a Shape object. This version of the code replaces the separate variables (indicated by striking them out) with a single Shape pointer variable (highlighted in yellow). Each instantiated shape object (Circle, Rectangle, or Triangle) is upcast to the Shape type. Upcasting and pointer variables are two requirements of polymorphism.

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();
Drawing shapes with and without polymorphism. The first version, copied from Figure 1(c), draws one of three shapes without using polymorphism. Adding a new shape to the system requires locating the switch wherever the program draws a shape and adding a new case. In the second version, pointer s can point to any specified shape Shape (a Circle, a Rectangle, or a Triangle). Furthermore, it can point to any subclass of Shape that defines a draw function.

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."

Adding A New Shape With Polymorphism

polymorphism

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?

  1. Create a new Ellipse class, make it a subclass of Shape, similar to the Circle, Rectangle, and Triangle classes illustrated in Figure 2. The new Ellipse class must override the virtual Shape draw function; the virtual is optional.
  2. Add a new case to the prompt and a new instantiation case to the Figure 3 code.
  3. The function call illustrated in Figure 4(c) requires no modification!

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.

UML class diagram of three classes:

Employee
-----------------
-----------------
+calc_pay() : double

SalariedEmployee
-----------------
-salary : double
-----------------
+calc_pay() : double

SalesEmployee
-----------------
-commission : double
-----------------
+calc_pay() : double

SalesEmployee is a subclass of SalariedEmployee, and SalariedEmployee is a subclass of Employee.
Different kinds of employees. Salaried employees earn a salary, and sales employees earn a salary plus a commission.

Accessing Member Variables Without Downcasting

polymorphism, member variables, downcasting

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)
Overriding and chaining calc_pay functions. Each function can access the private member variables in its defining class, and a subclass function can call the overridden superclass function using the superclass name and the scope resolution operator (highlighted).
  1. Calculates the pay for a SalariedEmployee using the private member variable salary.
  2. Calculates the pay for a SalesEmployee by calling the SalariedEmployee calc_pay() function and adding the private member variable commission.

Safe Downcasting With Polymorphism

polymorphism, downcasting, safe downcasting, typeid, dynamic_cast

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)
Safe downcasting with typeid or dynamic_cast. Both typeid and dynamic_cast require that the classes have at least one virtual function to identify a safe downcast.
  1. The 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 class
    If the two type_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).
  2. 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.

Identifying Polymorphism

polymorphism, requirements, inheritance, overriding, upcasting, pointer, reference, virtual

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:

  1. Inheritance - this requirement is satisfied by adding the Shape class and making it the superclass of the specialized shapes.
  2. Function overriding - each class provides an overridden version of the draw function. The function override is necessary because the compiler always begins searching in the symbol table with the pointer-variable class type. So, in this example, the compiler looks up the Shape symbol in the symbol table, and compilation fails if it doesn't find a draw function. Once it finds a virtual function, draw for example, polymorphism takes over.
  3. Upcasting - illustrated below with assignment and a function call.
  4. A pointer or reference variable - variable s assignment and a function call below.
  5. A virtual function - in Figure 2 all versions of draw are virtual.

  1. Identify inheritance by looking at the class specifications (typically in header files), diagrams, or program documentation.
  2. Identify function overriding by looking at member functions (typically in header or source-code file), class diagrams, or program documentation.
  3. Locate upcasting in the source code. See the examples in the table below.
  4. Verify that the object variables are either pointers or references in the source code calling the member function.
  5. Verify that the functions are virtual. Look for the virtual keyword in the superclass class specification (typically in a header file).

Assignment Function Call
Shape* s = new Circle(...);
 
void render(Shape* s) {...}
render(new Circle(...));
Example vs. typical Upcasting. Upcasting is easy to see when done with an assignment operation, but upcasting most often occurs when a function call passes a subclass object to a superclass function parameter.

Non-polymorphic Polymorphic
s->draw(); s->draw();
Polymorphism: early vs. late binding. Without polymorphism, the pointer variable's class type determines which function is called (blue). In this case, the function call is bound to a function at compile time (early binding). When all five requirements necessary for polymorphism are present, it is the object itself that determines which function is called (red). In this case, function binding can only occur at run time (late binding).

Use the checklist to answer the questions in the following self-test.

Downloadable Polymorphism Example Code

shape-non-poly.cpp, shape-poly.cpp, cast.cpp
ViewDownloadComments
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