Previous Table of Contents Next


7.5.2. Dynamic Binding

Deriving a class from another immediately establishes a hierarchy. We typically use such hierarchies to capture common ways of thinking about data. For example, we can imagine a hierarchy such as,

   class Vehicle { /* ... */ };
   class Aircraft: public Vehicle { /* ... */ };
   class Airplane: public Aircraft { /* ... */ };
   class Helicopter: public Aircraft { /* ... */ };
   class RoadVehicle: public Vehicle { /* ... */ };
   class Automobile: public RoadVehicle { /* ... */ };
   class Truck: public RoadVehicle { /* ... */ };

and so on. If we were writing programs to deal with vehicles, we can imagine that sometimes we might care what a vehicle was, say, a Helicopter, because helicopters can hover and other kinds of aircraft cannot. ‹Other times, we might care only that a vehicle was an Aircraft; and sometimes, it might interest us only that it was a Vehicle.

7.5.2.1. Virtual Functions

C++ allows us to look at objects in different levels of detail at different times by offering two facilities:

  If class D is publicly derived from class B, a pointer (or reference) to a D object can beonverted into a pointer (or reference) to a B object. If we have a pointer to B that actually points to a D object, we can convert that pointer back into a pointer to D.
  If class B has a virtual function and we have a pointer to B that actually points to a D object, using that pointer to call the virtual function will actually call the function defined in class D.

Here is a simple example that illustrates both of these facilities. First, we define two simple classes, each of which has two member functions:

   class B {
   public:
       void f() { cout << “B::f” << endl; }
       virtual void g() { cout << “B::g” << endl; }
   };
   class D: public B {
   public:
       void f() { cout << “D::f” << endl; }
       virtual void g() { cout << “D::g” << endl; }
   };

Strictly speaking, we did not need to declare D::g to be virtual; once a function is declared as virtual in a base class, all functions with the same name and argument types in all derived classes are virtual also. Notice that we have defined functions in class D with the same name as functions in class B. This is no problem in general, because classes are scopes and names defined in derived classes hide the corresponding names in the base classes.

Now we define a B object and a D object and make a B pointer point to each one:

   int main() {
       B b;
       D d;

       B* p1 = &b;
       B* p2 = &d;
       // ...

At this point, p1 and p2 are pointers, each of which nominally points at a B object. What happens if we use these pointers to call the f and g functions?

    // ...
    p1->f(); // B::f
    p1->g(); // B::g
    p2->f(); // B::f
    p2->g(); // D::g (!)
   }

There should be no question about what the calls do that use p1, because p1 is declared to point at a B object and actually does so. Indeed, we could remove the definition of class D entirely and the calls that use p1 would still work the same way.

The interesting part is when we use p2, because although we said that the type of p2 is a pointer to a B object, p2 actually points at a D object. Because every D is a kind of B, we can ordinarily think of p2 as pointing at “the B part of a D object.” If we view it that way, it is not surprising that calling p2->f() should print B::f. However, when we declared B::g to be virtual, we said that we want the particular function that we call to be determined by the type of the object, not by the type of the pointer. The effect is therefore to call D::g.

In order for virtual function calls to work efficiently, the language imposes two requirements. First, the pointer must actually point at an object from the appropriate inheritance hierarchy (in this case, the hierarchy rooted in B). That is why it is possible to convert a pointer to D into a pointer to B, but similar conversions between other pointer types are not possible. Second, the argument and result types for virtual functions must match in the base and derived classes.5 This allows the compiler to generate code that decides only which function to call, without having to generate, code to convert the arguments dynamically to appropriate types.


5There is a fairly obscure exception to this rule: If D is derived from B, D1 is derived from B1, and a virtual function in B returns a pointer or reference to B1, then the corresponding virtual function in D is allowed to return a pointer or reference to D1.

The C++ virtual function mechanism is designed to offer a useful compromise between speed and flexibility. The only context in which a virtual function call can take place is if it is being called through a pointer or a reference to a base class. This call can happen only after the compiler has seen the declaration of the base class, which means that the compiler knows all the virtual functions in that base class. Moreover, when the compiler is compiling each derived class, it also knows the definition of the base class, which means that it can match virtual functions in the derived class with the corresponding ones in the base class.

The effect of these requirements is to make it possible to call a virtual function without ever having to search an associative data structure during execution. A typical way of implementing virtual functions is to give each object that has any virtual functions at all a single pointer that points to a compiler-generated table that identifies the type of that object. That table will have one entry for each virtual function in that object’s class. Because the compiler knows all the relevant virtual functions when it sees the class definition, it is possible to allocate entries in the table during compilation. That means that calling a virtual function is just a matter of fetching the pointer from the object, fetching the address of the virtual function from an offset into the table that has been predetermined at compile time, and jumping to the appropriate function. The additional cost of a virtual function call varies with the implementation, but will typically be about the same as calling an ordinary function with no arguments and no result.


Previous Table of Contents Next