Part E - Polymorphism

Virtual Functions

Describe the difference between early and late function binding
Describe the syntax for implementing late binding of member functions
Distinguish monomorphic and polymorphic objects

"Respecting the inclusion relationship implies substitutability - operations that apply to entire sets should apply to any of their subsets as well." (Sutter, Alexandrescu, 2005)

Member Function Bindings | Polymorphic Objects | Coding Efficiency | Summary | Exercises


C++ implements inclusion polymorphism using a set of member functions that are virtual.  A virtual set may have different member function definitions associated with different dynamic types.  The object's static type identifies the hierarchy to which the set of definitions belongs.  The object's dynamic type identifies the particular definition to which the language binds a call at run-time. 

This chapter describes compile-time and run-time bindings, their relation to static and dynamic types and polymorphic objects.  Enabling the virtual mechanism does require programmer intervention in C++, but is especially straightforward.


Member Function Bindings

The compiler binds a call on an object to a member function's definition based on the object's type.  This binding can take either of two forms:

  • early binding - based on the object's static type
  • late binding - based on the object's dynamic type

Early Binding

Consider the following implementation of our Student class.  The header file is the same as in the chapter on Functions in a Hierarchy:

 // Student.h

 #include <iostream>
 const int N = 30;
 const int M = 13;

 class Person {
     char person[N+1];
   public:
     Person();
     Person(const char*);
     void display(std::ostream&) const; 
 };

 class Student : public Person {
     int no;
     char grade[M+1];
   public:
     Student();
     Student(const char*, int, const char*); 
     void display(std::ostream&) const;
 };

The implementation file is also the same as in the chapter on Functions in a Hierarchy:

 // Student.cpp

 #include <cstring>
 #include "Student.h"

 Person::Person() {
     person[0] = '\0';
 }

 Person::Person(const char* nm) {
     std::strncpy(person, nm, N);
     person[N] = '\0';
 }

 void Person::display(std::ostream& os) const {
     os << person << ' ';
 }

 Student::Student() {
     no = 0;
     grade[0] = '\0';
 }

 Student::Student(const char* nm, int n, const char* g) : Person(nm) { 
     // see Current Object chapter for validation logic
     no = n;
     std::strcpy(grade, g);
 }

 void Student::display(std::ostream& os) const {
     Person::display(os);
     os << no << ' ' << grade;
 }

Now consider the global show() function in the client code listed below.  The main() function calls this show() function twice, firstly for a Student object and secondly for a Person object.  Irrespective of the type of the argument passed to this global function, the compiler binds the call to the Person version of display().  That is, the compiler uses the type of the parameter received by show() to determine which member function definition to call.  This early binding is determined at compile time.  The client program produces the result shown on the right:

 // Early Binding
 // earlyBinding.cpp

 #include <iostream>
 #include "Student.h"

 void show(Person& person) {
     person.display();
     std::cout << std::endl;
 }

 int main() {
     Student harry("Harry", 975, "ABBAD");
     Person jane("Jane Doe");

     show(harry);
     show(jane);
 }















 Harry
 Jane Doe

C++ has selected the version of display() based on the static type (Person).  This default determination is the most efficient form of binding a member function's definition to a call, since it is selected at compile-time.

Note that shadowing does not occur inside the global show() function.  show() has no way of knowing which version of display() to select beyond the parameter's type.  (To demonstrate shadowing, add the statements harry.display() and jane.display() to the main() function.)

Late Binding

C++ provides the keyword virtual for overriding default early binding.  If this keyword is present, the compiler postpones binding until run-time when the code knows the object's dynamic type.  The run-time code then binds the call to the most derived version of display() based on the object's dynamic type. 

For example, in the following class definition the keyword virtual instructs the compiler to postpone binding of any call to the display() member function definitions until run-time and only then bind based on the dynamic type: 

 // Student.h

 #include <iostream>
 const int N = 30;
 const int M = 13;

 class Person {
     char person[N+1];
   public:
     Person();
     Person(const char*);
     virtual void display(std::ostream&) const; 
 };

 class Student : public Person {
     int no;
     char grade[M+1];
   public:
     Student();
     Student(const char*, int, const char*); 
     void display(std::ostream&) const;
 };

The implementation file and the client program do not change.  However, the show() function in the client program now calls the most derived version of display() based on the type of the argument passed to the show() function and produces the result shown on the right:

 // Late Binding
 // lateBinding.cpp

 #include <iostream>
 #include "Student.h"

 void show(Person& person) {
     person.display();
     std::cout << std::endl;
 }

 int main() {
     Student harry("Harry", 975, "ABBAD");
     Person jane("Jane Doe");

     show(harry);
     show(jane);
 }















 Harry 975 ABBAD
 Jane Doe

The first call (show(harry)) passes a reference to a Student, while the second call (show(jane)) passes a reference to a Person.  In each case, the version of display() called at run time is the most derived version for the type referenced by the parameter in show()

Note that the argument to the show() function is passed by reference.  If we passed the argument by value (that is, if the parameter held a base class copy of the argument), the show() function would still call the most derived version of display(), but that version would then be the Person version. 

Documentation

We can identify a member function as virtual even if no derived class exists.  Some programmers include the qualifier virtual in derived class prototypes as a form of documentation.  This improves readability but has no syntactic effect. 

Virtual Tables

The compiler implements late binding by inserting a virtual table.  This table holds the information for selecting the appropriate member function definition.  The run-time code selects the entry in this table based upon the object's dynamic type at the time and thereby calls the most appropriate member function definition. 

A virtual table amounts to the equivalent of a single function call and does not introduce a significant overhead. 

Overriding a Late Binding

To override a late binding with an early binding in a particular function, we specify the scope explicitly:

 void show(Person& person) {
     person.Person::display();
 }

Polymorphic Objects

A polymorphic object changes its dynamic type under programmer control.  The region of memory that the object occupies depends on the programmer's allocation; that is, on the constructor that the programmer has called in allocating dynamic memory.  In C++, the region of memory varies in size with the dynamic type. 

A polymorphic object's static type identifies the hierarchy to which the object belongs, regardless of its current dynamic type.  We specify the object's static type through any one of

  • a pointer to the object
  • a receive-by-address parameter
  • a receive-by-reference parameter

For example, the highlighted code specifies the static type on any object pointed to by person:

 // Polymorphic Objects - Static Type

 #include <iostream>
 #include "Student.h"

 int main() {
     Person* person = nullptr;

     // ...

 }

The highlighted code specifies the dynamic type.  The results produced by this code are listed on the right:

 // Polymorphic Objects - Dynamic Type
 // dyanmicType.cpp

 #include <iostream>
 #include "Student.h"

 void show(Person& person) {
     person.display();
     std::cout << std::endl;
 }

 int main() {
     Person* person = nullptr;

     person = new Student("Harry", 975, "ABBAD");
     show(*person);
     delete person;

     person = new Person("Jane Doe");
     show(*person);
     delete person;
 }















 Harry 975 ABBAD



 Jane Doe


In the main() function, person is a pointer to a Person (the static type) with no memory initially allocated.  At this execution point the object's dynamic type is unknown.  After the first allocation, person points to a Student type (dynamic type).  After the second allocation, person points to a Person type (the new dynamic type).  The static and dynamic types are related through the hierarchy.  Only one show() function is needed to display both dynamic types.  We let the virtual table bind the most derived definition of display() within the show() function at run time.

In this example, person points to a polymorphic object throughout its lifetime.  This object can assume any one of an infinite number of types as long as they are derived from a Person type.  show() is a polymorphic function with a parameter that receives a reference to an infinite number of types derived from a Person type. 

Virtual Destructors

If a derived class allocates resources, its own destructor typically releases those resources.  To ensure that the derived class destructor is called for a polymorphic object, we declare the base class destructor virtual.  Since a derived class destructor automatically calls its immediate base class' destructor, all destructors in the object's hierarchy will be called automatically. 

Good design codes the destructor in a base class as virtual, even if no class is currently derived from that base class.  The presence of a virtual base class destructor ensures that the most derived destructor will be called if and when a class is derived from the base class without requiring an upgrade to the definition of the base class.


Efficiency and Flexibility

The efficiency and flexibility introduced by inclusion polymorphism is worthwhile noting.  Virtual functions reduce code size considerably.  Our show() function works on objects that range in type across the entire hierarchy.  We define member functions (display()) for those classes within the hierarchy that require distinct processing and only those classes.  During the lifecycles of the client applications that use our hierarchy, we may add several classes to that hierarchy.  Our original coding, without alteration, selects the most derived version of the member function in each upgrade of the hierarchy. 

It is the versatility of the virtual mechanism that is so invaluable to object-oriented programming.


Summary

  • early bindings of a call to a member function's definition occur at compile-time
  • late bindings of a call to a member function's definition occur at run-time
  • the keyword virtual on a member function's declaration specifies late binding
  • the static type of a polymorphic object is the type of the object's pointer
  • the dynamic type of a polymorphic object is the type specified in the allocation of dynamic memory
  • declare a base class destructor virtual even if there are no derived classes

Exercises




Previous Reading  Previous: Overview of Polymorphism Next: Abstract Base Classes   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   
Logo
Creative Commons License