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. Stated without detail, polymorphism is just a way of choosing which function to execute when multiple functions match a function call. Remarkably, it permits the program to delay choosing a function until runtime - until the moment of the call itself! You ask yourself, "Why wouldn't I know which function to call?" Let's set the stage by posing a problem and outlining a solution that doesn't use polymorphism. Later, we'll revisit the problem and see how a polymorphic solution can simplify the program.
Our proposed program must perform three tasks:
class Circle { public: void draw(); }; |
class Rectangle { public: void draw(); }; |
class Triangle { 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; }
Complete demonstration program: shape-proc.cpp
Figure 3 partially answers the question, "Why wouldn't I know which function to call?" The programmer 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 select the correct draw function from several choices while running. Our current program does this but with more variables than necessary. Furthermore, imagine that the user wishes to add another shape to the program: an Ellipse. What code must we modify?
Steps 1 and 2 are inescapable regardless of how we write the code. Fortunately, both steps are localized - each step requires editing code in one place in one file. In this unrealistically simplified example, step 3 seems on par with the first two, but this is not the case. The switch statement illustrated in step 3 illustrates using the object once. An authentic program must repeat the switch statement whenever and wherever the program uses the user's shape. Furthermore, real programs have many more functions than this simple example. The functions are generally larger and use the program objects repeatedly, making the objects more difficult to locate and update. Each time the program calls a function, a code block similar to Figure 3 is necessary. This code structure is bulky and cumbersome, even without modifying the program.
Now imagine that our program consists of scores of files, each consisting of hundreds or thousands of lines of code, which is common in "real world" programs. When we modify such a program, there is a real risk that programmers may overlook some instances of the Figure-3-like code and fail to update them, potentially introducing a bug that might not be detected until a catastrophic failure occurs. The problem represented by Figure 3 arises from the practice of binding a function call to a specific function too early at compile time.
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
c->draw();
r->draw();
t->draw();
appearing in Figure 3, the compiler can bind 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 works most of the time - all the functions we have written this semester are processed this way. But, as Figure 3 demonstrates, compile-time binding doesn't always work and is sometimes inconvenient and awkward. Polymorphism allows us to write more elegant code that is easier to maintain.
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. We have previously explored some of those requirements, but others are new, and we'll explore them in greater detail in subsequent sections.
These required features roughly outline the remainder of the chapter.
The terms overloading and overriding are similar and therefore easily confused. Let's take a moment and review their similarities and their differences. You might find it worthwhile to review Figure 2 where the terms were first introduced.
Overloaded Functions | Overridden Functions |
---|---|
|
|
Although polymorphism requires a function override, programs may override functions without making them polymorphic.