Which block of executable code gets used when a program calls a function? The compiler has the responsibility of answering this question. Interpreting a function call in the source code as executing a particular block of function code is termed binding the function name. With C, the task is simple because each function name corresponds to a distinct function. With C++, the task is more complex because of function overloading. The compiler has to look at the function arguments as well as the function name to figure out which function to use. Nonetheless, this kind of binding is a task a C or C++ compiler could perform during the compiling process; binding that takes place during compilation is called static binding (or early binding). However, virtual functions make the job more difficult yet. As shown in Listing 13.10, the decision of which function to use can’t be made at compile time because the compiler doesn’t know which kind of object the user is going to choose to make. Therefore, the compiler has to generate code that allows the correct virtual method to be selected as the program runs; this is called dynamic binding (or late binding).
Now that you’ve seen virtual methods at work, let’s look at this process in greater depth, beginning with how C++ handles pointer and reference type compatibility.
Pointer and Reference Type Compatibility
Dynamic binding in C++ is associated with methods invoked by pointers and references, and this is governed, in part, by the inheritance process. One way public inheritance models the is-a relationship is in how it handles pointers and references to objects. Normally, C++ does not allow you to assign an address of one type to a pointer of another type. Nor does it let a reference to one type refer to another type:
double x = 2.5;
int * pi = &x // invalid assignment, mismatched pointer types
long & rl = x; // invalid assignment, mismatched reference type
However, as you’ve seen, a reference or a pointer to a base class can refer to a derived-class object without using an explicit type cast. For example, the following initializations are allowed:
BrassPlus dilly ("Annie Dill", 493222, 2000);
Brass * pb = &dilly // ok
Brass & rb = dilly; // ok
Converting a derived-class reference or pointer to a base-class reference or pointer is called upcasting, and it is always allowed for public inheritance without the need for an explicit type cast. This rule is part of expressing the is-a relationship. A BrassPlus object is a Brass object in that it inherits all the data members and member functions of a Brass object. Therefore, anything that you can do to a Brass object, you can do to a BrassPlus object. So a function designed to handle a Brass reference can, without fear of creating problems, perform the same acts on a BrassPlus object. The same idea applies if you pass a pointer to an object as a function argument. Upcasting is transitive. That is, if you derive a BrassPlusPlus class from BrassPlus, then a Brass pointer or reference can refer to a Brass object, a BrassPlus object, or a BrassPlusPlus object.
The opposite process, converting a base-class pointer or reference to a derived-class pointer or reference, is called downcasting, and it is not allowed without an explicit type cast. The reason for this restriction is that the is-a relationship is not, in general, symmetric. A derived class could add new data members, and the class member functions that used these data members wouldn’t apply to the base class. For example, suppose you derive a Singer class from an Employee class, adding a data member representing a singer’s vocal range and a member function, called range(), that reports the value for the vocal range. It wouldn’t make sense to apply the range() method to an Employee object. But if implicit downcasting were allowed, you could accidentally set a pointer-to-Singer to the address of an Employee object and use the pointer to invoke the range() method (see Figure 13.4).
Upcasting also takes place for function calls with base-class references or pointers as parameters. Consider the following code fragment, and suppose each function calls upon the virtual method ViewAcct():
void fr(Brass & rb); // uses rb.ViewAcct()
void fp(Brass * pb); // uses pb->ViewAcct()
void fv(Brass b); // uses b.ViewAcct()
int main()
{
Brass b("Billy Bee", 123432, 10000.0);
BrassPlus bp("Betty Beep", 232313, 12345.0);
fr(b); // uses Brass::ViewAcct()
fr(bp); // uses BrassPlus::ViewAcct()
fp(b); // uses Brass::ViewAcct()
fp(bp); // uses BrassPlus::ViewAcct()
fv(b); // uses Brass::ViewAcct()
fv(bp); // uses Brass::ViewAcct()
...
}