Inapoi
Inainte
Cuprins
15: Polymorphism &
Virtual Functions
Polymorphism (implemented in C++
with
virtual
functions) is the third essential feature of an object-oriented programming
language, after data abstraction and inheritance.
It provides another dimension of
separation of interface from implementation, to decouple what from
how. Polymorphism allows improved code organization and readability as
well as the creation of extensible programs that can be
“grown” not only during the original creation of the project, but
also when new features are desired.
Encapsulation creates new data types by
combining characteristics and behaviors. Access control separates the interface
from the implementation by making the details private. This kind of
mechanical organization makes ready sense to someone with a procedural
programming background. But virtual functions deal with
decoupling in terms of types. In Chapter 14, you
saw how inheritance allows the treatment of an object as its own type or
its base type. This ability is critical because it allows many types (derived
from the same base type) to be treated as if they were one type, and a single
piece of code to work on all those different types equally. The virtual function
allows one type to express its distinction from another, similar type, as long
as they’re both derived from the same base type. This distinction is
expressed through differences in behavior of the functions that you can call
through the base class.
In this chapter, you’ll learn about
virtual functions, starting from the basics with simple examples that strip away
everything but the “virtualness” of the
program.
Evolution of C++ programmers
C
programmers seem to acquire C++
in three steps. First, as simply a “better C,” because C++ forces
you to declare all functions before using them and is much pickier about how
variables are used. You can often find the errors in a C program simply by
compiling it with a C++ compiler.
The second step is
“object-based” C++.
This means that you easily see the code organization benefits of grouping a data
structure together with the functions that act upon it, the value of
constructors and destructors, and perhaps some simple inheritance. Most
programmers who have been working with C for a while quickly see the usefulness
of this because, whenever they create a library, this is exactly what they try
to do. With C++, you have the aid of the compiler.
You can get stuck at the object-based
level because you can quickly get there and you get a lot of benefit without
much mental effort. It’s also easy to feel like you’re creating data
types – you make classes and objects, you send messages to those objects,
and everything is nice and neat.
But don’t be fooled. If you stop
here, you’re missing out on the greatest part of the language, which is
the jump to true object-oriented programming. You can do this only with virtual
functions.
Virtual
functions enhance the concept of
type instead of just encapsulating code inside structures and behind walls, so
they are without a doubt the most difficult concept for the new C++ programmer
to fathom. However, they’re also the turning point in the understanding of
object-oriented programming. If you don’t use virtual functions, you
don’t understand OOP yet.
Because the virtual function is
intimately bound with the concept of type, and type is at the core of
object-oriented programming, there is no analog to the virtual function in a
traditional procedural language. As a procedural programmer, you have no
referent with which to think about virtual functions, as you do with almost
every other feature in the language. Features in a procedural language can be
understood on an algorithmic level, but virtual functions can be understood only
from a design
viewpoint.
Upcasting
In Chapter 14 you saw how an object can
be used as its own type or as an object of its base type. In addition, it can be
manipulated through an address of the base type. Taking the address of an object
(either a pointer or a reference) and treating it as the address of the base
type is called upcasting because of the way
inheritance trees are drawn with the base class at the top.
You also saw a problem arise, which is
embodied in the following code:
//: C15:Instrument2.cpp
// Inheritance & upcasting
#include <iostream>
using namespace std;
enum note { middleC, Csharp, Eflat }; // Etc.
class Instrument {
public:
void play(note) const {
cout << "Instrument::play" << endl;
}
};
// Wind objects are Instruments
// because they have the same interface:
class Wind : public Instrument {
public:
// Redefine interface function:
void play(note) const {
cout << "Wind::play" << endl;
}
};
void tune(Instrument& i) {
// ...
i.play(middleC);
}
int main() {
Wind flute;
tune(flute); // Upcasting
} ///:~
The function tune( ) accepts
(by reference) an
Instrument, but also without complaint anything derived from
Instrument. In main( ), you can see this happening as a
Wind object is passed to tune( ), with no
cast necessary. This is acceptable; the interface in
Instrument must exist in Wind, because Wind is publicly
inherited from Instrument. Upcasting from Wind to
Instrument may “narrow” that interface, but never less than
the full interface to Instrument.
The
same arguments are true when dealing with pointers; the only difference is that
the user must explicitly take the addresses of objects as they are passed into
the
function.
The problem
The problem with Instrument2.cpp
can be seen by running the program. The output is Instrument::play. This
is clearly not the desired output, because you happen to know that the object is
actually a Wind and not just an Instrument. The call should
produce Wind::play. For that matter, any object of a class derived from
Instrument should have its version of play( ) used,
regardless of the situation.
The behavior of Instrument2.cpp is
not surprising, given C’s approach to functions. To understand the issues,
you need to be aware of the concept of
binding.
Function call binding
Connecting a function call to a function
body is called binding. When binding is performed before the program is
run (by the compiler and linker), it’s called early
binding. You may not have heard the term before
because it’s never been an option with procedural languages: C compilers
have only one kind of function call, and that’s early
binding.
The problem in the program above is
caused by early binding because the compiler cannot know the correct function to
call when it has only an Instrument address.
The solution is called late
binding, which means the
binding occurs at runtime, based on the type of the object. Late binding is also
called dynamic binding or
runtime binding. When a
language implements late binding, there must be some mechanism to determine the
type of the object at runtime and call the appropriate member function. In the
case of a compiled language, the compiler still doesn’t know the actual
object type, but it inserts code that finds out and calls the correct function
body. The late-binding mechanism varies from language to language, but you can
imagine that some sort of type information must be installed in the objects.
You’ll see how this works
later.
 |
|