course of C++ programming language
lecture 10: polymorphism and virtual functions
Introduction
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. On this lecture, 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.
Virtual functions
A virtual function is a member function that is declared within a base class and redefined by a derived class. To create a virtual function, precede the function's declaration in the base class with the keyword virtual. When a class containing a virtual function is inherited, the derived class redefines the virtual function to fit its own needs. The virtual function within the base class defines the form of the interface to that function. Each redefinition of the virtual function by a derived class implements its operation as it relates specifically to the derived class.
The compiler will guarantee the correct correspondence between objects and the member functions applied to them.
A virtual function can be used even if no class is derived from its class, and a derived class that does not need its own version of a virtual function need not provide one. When deriving a class, simply provide an appropriate function, if it is needed.
A function from a derived class with the same name and the same set of argument
types as a virtual function in a base is said to override the base
class version of the virtual function.
Except where we explicitly say which version of a virtual function is called
(as in the call Cat::move()
), the overriding function is chosen as
the most appropriate for the object for which it is called.
Getting "the right" behavior from Animal
's functions
independently of exactly what kind of Animal
is actually used is
called polymorphism.
A type with virtual functions is called a polymorphic type.
To get polymorphic behavior in C++, the member functions called must be
virtual and objects must be manipulated through pointers or
references.
When manipulating an object directly (rather than through a pointer or
reference), its exact type is known by the compilation so that run-time
polymorphism is not needed.
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.
The binding occurs at runtime is called late binding (it base 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.
Clearly, to implement polymorphism, the compiler must store some kind of type
information in each object of class Animal
and use it to call
the right version of the virtual function move()
.
In a typical implementation, the space taken is just enough to hold a pointer.
This space is taken only in objects of a class with virtual functions - not in
every object, or even in every object of a derived class.
You pay this overhead only for classes for which you declare virtual functions.
Abstract classes
Some classes, such as class Shape
represent abstract concepts for
which objects cannot exist.
A Shape
makes sense only as the base of some class derived from it.
You can declare the virtual functions of class Shape
to be
pure virtual functions.
A virtual function is "made pure" by the initializer =0
.
A class with one or more pure virtual functions is an abstract class, and no objects of that abstract class can be created.
A n abstract class can be used only as an interface and as a base for other classes.
A pure virtual function that is not defined in a derived class remains a pure virtual function, so the derived class is also an abstract class.
An important use of abstract classes is to provide an interface without exposing any implementation details.
Virtual destructors
You cannot use the virtual keyword with constructors, but destructors can and often must be virtual.
The constructor has the special job of putting an object together piece-by-piece, first by calling the base constructor, then the more derived constructors in order of inheritance (it must also call member-object constructors along the way). Similarly, the destructor has a special job: it must disassemble an object that may belong to a hierarchy of classes. To do this, the compiler generates code that calls all the destructors, but in the reverse order that they are called by the constructor. That is, the destructor starts at the most-derived class and works its way down to the base class. This is the safe and desirable thing to do because the current destructor can always know that the base-class members are alive and active. If you need to call a base-class member function inside your destructor, it is safe to do so. Thus, the destructor can perform its own cleanup, then call the next-down destructor, which will perform its own cleanup, etc. Each destructor knows what its class is derived from, but not what is derived from it.
You should keep in mind that constructors and destructors are the only places where this hierarchy of calls must happen (and thus the proper hierarchy is automatically generated by the compiler). In all other functions, only that function will be called (and not base-class versions), whether it's virtual or not. The only way for base-class versions of the same function to be called in ordinary functions (virtual or not) is if you explicitly call that function.
Normally, the action of the destructor is quite adequate. But what happens if you want to manipulate an object through a pointer to its base class? This activity is a major objective in object-oriented programming. The problem occurs when you want to delete a pointer of this type for an object that has been created on the heap with new. If the pointer is to the base class, the compiler can only know to call the base-class version of the destructor during delete. This is the same problem that virtual functions were created to solve for the general case. Fortunately, virtual functions work for destructors as they do for all other functions except constructors.
When you run the program, you'll see that delete p
calls the
derived-class destructor followed by the base-class destructor, which is the
behavior we desire.
Forgetting to make a destructor virtual is an insidious bug because it
often doesn't directly affect the behavior of your program, but it can quietly
introduce a memory leak.
References
-
B.Stroustrup: The C++ programming language. Third edition.
Section 12: derived classes; pp. 301-325. -
Bruce Eckel: Thinking in C++. Second edition.
Section 15: polymorphism and virtual functions; pp. 627-681.