Polymorphism is accomplished through an elegant data structure involving three levels of pointers. We’ve discussed one level—the function pointers in the vtable. These point to the actual functions that execute when a virtual
function is invoked.
Now we consider the second level of pointers. Whenever an object of a class with one or more virtual
functions is instantiated, the compiler attaches to the object a pointer to the vtable for that class. This pointer is normally at the front of the object, but it isn’t required to be implemented that way. In Fig. 12.18, these pointers are associated with the objects created in Fig. 12.17 (one object for each of the types SalariedEmployee
, CommissionEmployee
and BasePlusCommissionEmployee
). The diagram displays each of the object’s data member values. For example, the salariedEmployee
object contains a pointer to the SalariedEmployee
vtable; the object also contains the values John Smith
, 111-11-1111
and $800.00
.
The third level of pointers simply contains the handles to the objects that receive the virtual
function calls. The handles in this level may also be references. Fig. 12.18 depicts the vector employees
that contains Employee
pointers.
Now let’s see how a typical virtual
function call executes. Consider the call baseClassPtr->print()
in function virtualViaPointer
(line 69 of Fig. 12.17). Assume that baseClassPtr
contains employees[ 1 ]
(i.e., the address of object commissionEmployee
in employees
). When the compiler compiles this statement, it determines that the call is indeed being made via a base-class pointer and that print
is a virtual
function.
The compiler determines that print
is the second entry in each of the vtables. To locate this entry, the compiler notes that it will need to skip the first entry. Thus, the compiler compiles an offset or displacement into the table of machine-language object-code pointers to find the code that will execute the virtual
function call. The size in bytes of the offset depends on the number of bytes used to represent a function pointer on an individual platform. For example, on a 32-bit platform, a pointer is typically stored in four bytes, whereas on a 64-bit platform, a pointer is typically stored in eight bytes. We assume four bytes for this discussion.
The compiler generates code that performs the following operations [Note: The numbers in the list correspond to the circled numbers in Fig. 12.18]:
1. Select the ith entry of employees
(in this case, the address of object commissionEmployee
), and pass it as an argument to function virtualViaPointer
. This sets parameter baseClassPtr
to point to commissionEmployee
.
2. Dereference that pointer to get to the commissionEmployee
object—which, as you recall, begins with a pointer to the CommissionEmployee
vtable.
3. Dereference commissionEmployee
’s vtable pointer to get to the CommissionEmployee
vtable.
4. Skip the offset of four bytes to select the print
function pointer.
5. Dereference the print
function pointer to form the “name” of the actual function to execute, and use the function call operator ()
to execute the appropriate print
function, which in this case prints the employee’s type, name, social security number, gross sales and commission rate.
Fig. 12.18’s data structures may appear to be complex, but this complexity is managed by the compiler and hidden from you, making polymorphic programming straightforward. The pointer dereferencing operations and memory accesses that occur on every virtual
function call require some additional execution time. The vtables and the vtable pointers added to the objects require some additional memory.
Performance Tip 12.1
Polymorphism, as typically implemented with virtual functions and dynamic binding in C++, is efficient. You can use these capabilities with nominal impact on performance.
Performance Tip 12.2
Virtual functions and dynamic binding enable polymorphic programming as an alternative to switch logic programming. Optimizing compilers normally generate polymorphic code that’s nearly as efficient as hand-coded switch-based logic. Polymorphism’s overhead is acceptable for most applications. In some situations—such as real-time applications with stringent performance requirements—polymorphism’s overhead may be too high.