How Virtual Functions Are Implemented

An obvious question about dynamic binding is “how much overhead is involved?” Is the added convenience gained at the expense of execution speed? Fortunately, virtual functions are very efficient, so calling one takes only slightly longer than calling a normal function.

In some situations, a virtual function call can be implemented as a normal function call; that is, using static binding. For example:

SalesPerson aSeller( "John Smith" );

SalesPerson *salePtr;

float salary;

salePtr = &aSeller;

salary = aSeller.computePay(); // Static binding possible

salary = salePtr->computePay(); // Static binding possible

In this example, the type of aSeller is known, so SalesPerson::computePay can be called directly. Similarly, the type of the object that salePtr points to is know, and again the function can be called directly. In situations where the compiler cannot use static binding, such as the statement person->computePay() in the earlier example, the compiler uses dynamic binding.

Dynamic binding is implemented in C++ through the use of a virtual function table, or a “v-table.” This is an array of function pointers that the compiler constructs for every class that uses virtual functions. For example, WageEmployee, SalesPerson, and Manager each have their own v-table.

The v-table contains one function pointer for each virtual function in the class. All of the employee classes have only one virtual function, so all of their v-tables contain just one pointer. Each pointer points to the version of the function that is appropriate to that class. Thus, the v-table for SalesPerson has a pointer to SalesPerson::computePay, and the v-table for Manager has a pointer to Manager::computePay. This is illustrated in Figure 7.3.

Note that it is not required for a derived class to redefine a virtual function declared in its base class. For example, suppose SalesPerson did not define a computePay function. Then SalesPerson's v-table would contain a pointer to WageEmployee::computePay. If WageEmployee in turn did not define computePay, both classes' v-tables would have pointers to Employee::computePay.

Each instance of a class contains a hidden pointer to the class's v-table. When a statement like person->computePay() is executed, the compiler dereferences the v-table pointer in the object pointed to by person. The compiler then calls the computePay function pointed to by the pointer in the v-table. In this way, the compiler calls a different function for each type of object.

The only difference between a normal function call and a virtual function call is the extra pointer dereferencing. A virtual function call usually executes as fast as or faster than the switch statement that would otherwise be used.