When a virtual function is called through a reference or pointer, the compiler generates code to decide at run time which function to call. The function that is called is the one that corresponds to the dynamic type of the object bound to that pointer or reference.
As an example, consider our print_total
function from §15.1 (p. 593). That function calls net_price
on its parameter named item
, which has type Quote&
. Because item
is a reference, and because net_price
is virtual, the version of net_price
that is called depends at run time on the actual (dynamic) type of the argument bound to item
:
Quote base("0-201-82470-1", 50);
print_total(cout, base, 10); // calls Quote::net_price
Bulk_quote derived("0-201-82470-1", 50, 5, .19);
print_total(cout, derived, 10); // calls Bulk_quote::net_price
In the first call, item
is bound to an object of type Quote
. As a result, when print_total
calls net_price
, the version defined by Quote
is run. In the second call, item
is bound to a Bulk_quote
object. In this call, print_total
calls the Bulk_quote
version of net_price
.
It is crucial to understand that dynamic binding happens only when a virtual function is called through a pointer or a reference.
base = derived; // copies the Quote part of derived into base
base.net_price(20); // calls Quote::net_price
When we call a virtual function on an expression that has a plain—nonreference and nonpointer—type, that call is bound at compile time. For example, when we call net_price
on base
, there is no question as to which version of net_price
to run. We can change the value (i.e., the contents) of the object that base
represents, but there is no way to change the type of that object. Hence, this call is resolved, at compile time, to the Quote
version of net_price
.
Key Concept: Polymorphism in C++
The key idea behind OOP is polymorphism. Polymorphism is derived from a Greek word meaning “many forms.” We speak of types related by inheritance as polymorphic types, because we can use the “many forms” of these types while ignoring the differences among them. The fact that the static and dynamic types of references and pointers can differ is the cornerstone of how C++ supports polymorphism.
When we call a function defined in a base class through a reference or pointer to the base class, we do not know the type of the object on which that member is executed. The object can be a base-class object or an object of a derived class. If the function is virtual, then the decision as to which function to run is delayed until run time. The version of the virtual function that is run is the one defined by the type of the object to which the reference is bound or to which the pointer points.
On the other hand, calls to nonvirtual functions are bound at compile time. Similarly, calls to any function (virtual or not) on an object are also bound at compile time. The type of an object is fixed and unvarying—there is nothing we can do to make the dynamic type of an object differ from its static type. Therefore, calls made on an object are bound at compile time to the version defined by the type of the object.
Virtuals are resolved at run time only if the call is made through a reference or pointer. Only in these cases is it possible for an object’s dynamic type to differ from its static type.