12.1. An Introduction To Polymorphism

polymorphism (definition), dynamic dispatch (definition), dispatch (definition), function binding
Time: 00:05:25 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides: PDF, PPTX
Review

The object-oriented paradigm has three defining characteristics: encapsulation, inheritance, and polymorphism. Encapsulation and inheritance can stand independently, but polymorphism requires inheritance. Although polymorphism is a potent tool, it isn't easy to fully appreciate its power outside large, complex programs. Therefore, this semester focuses on its syntax and behavior while forgoing more useful examples. Rest assured that subsequent courses will build on this basic foundation. Chapter 12 covers several topics, but they all lead up to and revolve around a central theme: polymorphism.

Polymorphism

polymorphism is just a way of choosing which function to execute when multiple functions match the same function call.

Three additional terms offer different perspectives on polymorphism, helping to build a deeper understanding of its behavior:
Dispatch
When a program calls a function, it's said to dispatch it.
Dynamic
An activity that occurs while the program is executing.
Function binding
The time when a program can connect a function call to the corresponding function's machine instructions.
The terms explain a synonym for polymorphism: dynamic dispatch. Given a specific set of requirements, described below, programs can have multiple functions that match a given function call. In this situation, the compiler can't bind the call to the correct function. Polymorphism delays function binding until the program is running and actually calls the function, dispatching it dynamically.

The text uses the simplified problem of drawing shapes to illustrate a situation where allowing a single function call to match multiple functions can simplify programming a solution. The illustration begins by posing the problem and outlining a solution that doesn't use polymorphism. After detailing the concept necessary for polymorphism, the text revisits the problem and illustrates how a polymorphic solution simplifies the program. The chapter concludes with an authentic example that is awkward and challenging to solve without polymorphism.

Drawing Shapes Without Polymorphism

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

The shape problem allows a user to select a provided shape and draw it on the screen. It must perform three distinct tasks:

  1. Specify multiple classes representing different, drawable shapes.
  2. Prompt the user to choose between one of the shapes and instantiate an object to represent the choice.
  3. Draw the selected shape. This task represents how a program might use an object. An authentic program may use an object many times from many locations within the program. Consequently, when we see the non-polymorphic solution, we must imagine it replicated many times and in many locations, possibly spanning numerous program files.
class Circle
{
    public:
	void draw();
};
cout << "C:\tCircle" << endl;
cout << "R:\tRectangle" << endl;
cout << "T:\tTriangle" << endl;

cout << "Please choose a shape: ";

char	choice;
cin >> choice;
cin.ignore();

Circle*		c = nullptr;
Rectangle*	r = nullptr;
Triangle*	t = nullptr;

switch (choice)
{
    case 'C' :
    case 'c' :
        c = new Circle(...);
        break;

    case 'R' :
    case 'r' :
        r = new Rectangle(...);
        break;

    case 'T' :
    case 't' :
        t = new Triangle(...);
        break;
}
switch (choice)
{
    case 'C' :
    case 'c' :
        c->draw();
        break;

    case 'R' :
    case 'r' :
        r->draw();
        break;

    case 'T' :
    case 't' :
        t->draw();
        break;
}
class Rectangle
{
    public:
	void draw();
};
class Triangle
{
    public:
	void draw();
};
(a)(b)(c)
A non-polymorphic shape-drawing solution. The figure outlines the three parts of the drawing solution with pseudocode.
  1. The initial shape classes. The program implements each class as a header file, and, if the drawing function is large, a source-code file. The classes are the same in the polymorphic and non-polymorphic programs.
  2. The code fragment prompts the user to select a shape and reads the choice. The switch statement interprets the user's choice by instantiating the appropriate shape. The salient concept for the demonstration is that the program doesn't "know" which shape users wish to draw until they choose it during program execution. The switch and the character variable driving it are necessary for both the program's polymorphic and non-polymorphic versions. The non-polymorphic solution also requires three variables, each a pointer to a different shape.
  3. The switch statement draws the shape based on the user's choice by calling the appropriate draw function, but, more generally, it demonstrates a program using an object. While the simple program implements this statement entirely main, an authentic program can repeat this code whenever and wherever it needs to use that object. Furthermore, a large object may have many functions, requiring different switches for each. The choice and shapes (c, r, and t) are necessary to draw the selected shape, so the program must pass them as arguments to every function using the objects. If the program uses the shape (or, more generally, an object) in many locations across numerous files, maintaining the function calls is very challenging. The complete non-polymorphic version of the program: shape-non-poly.cpp.

The previous figure illustrates a situation where multiple classes define functions with the same signature. Object-oriented programs specify the correct function by binding the call to an object with either the dot or arrow operator, which is generally adequate. However, in the shape example, the program "doesn't know" which function to call until the user selects a shape, which only happens after the program starts running. So, the program must be sufficiently dynamic to choose the correct draw function from several choices while it is running. The current program does this, but at the expense of passing many arguments. Now, imagine that the user wishes to add another shape to the program: an Ellipse. What code must programmers modify?

  1. Add a new Ellipse class similar to the classes illustrated above.
  2. Add a new choice to the prompt and a new case to the switch. The prompt, choice, and object instantiation occur at only one place in the program, making the changes straightforward.
  3. Locate every occurrence of the code represented by Figure 1(c) in the program and add a new case. It is this awkward, error-prone step that polymorphism relieves.
Adding a new shape class. Programs inevitably grow and evolve. A good, robust design allows programmers to make changes without introducing errors. Steps 1 and 2 are inescapably necessary for both polymorphic and non-polymorphic implementations. Fortunately, the modifications are localized, affecting only one or two files. Alternatively, in an authentic program, step 3 may require locating and modifying code throughout many files.

Function Binding, Part 1

polymorphism, function binding, compile-time binding, static binding, early binding

When the compiler processes a function, it generates the machine code from the function body, which the operating system loads into memory with the rest of the program when it runs. Normally, the compiler binds a function call to the function's machine code by generating instructions to jump to the code's memory location. The program executes the machine code before returning to the calling point. So, for the function calls

  1. c->draw();
  2. r->draw();
  3. t->draw();

the compiler binds statement 1 to the Circle draw function, statement 2 to Rectangle, and statement 3 to Triangle. Programmers use three terms to name and describe this kind of binding:

The terms are synonyms, meaning the compiler can bind a function call to a specific set of machine instructions when it compiles the program. Compile-time binding is generally sufficient, but, as the shape-drawing problem illustrates, it is sometimes inconvenient and awkward. Polymorphism provides a more elegant, easier-to-maintain solution.

Polymorphism Requirements

polymorphism

Polymorphism is active by default in Java programs, and programmers must explicitly deactivate it when it's not wanted. Conversely, before programmers can use polymorphism in a C++ program, they must activate it by satisfying five requirements.

Requirements For Polymorphism
  1. Inheritance
  2. Function overriding
  3. A pointer or reference variable (polymorphism cannot operate through an automatic or stack variable)
  4. Upcasting
  5. A virtual function

The text previously described the first three requirements, but others are new, and roughly outline the remainder of the chapter.