Team LiB
Previous Section Next Section

15.3. Virtual Functions

 
Image

As we’ve seen, in C++ dynamic binding happens when a virtual member function is called through a reference or a pointer to a base-class type (§15.1, p. 593). Because we don’t know which version of a function is called until run time, virtual functions must always be defined. Ordinarily, if we do not use a function, we don’t need to supply a definition for that function (§6.1.2, p. 206). However, we must define every virtual function, regardless of whether it is used, because the compiler has no way to determine whether a virtual function is used.

 

Exercises Section 15.2.3

 

Exercise 15.8: Define static type and dynamic type.

Exercise 15.9: When is it possible for an expression’s static type to differ from its dynamic type? Give three examples in which the static and dynamic type differ.

Exercise 15.10: Recalling the discussion from §8.1 (p. 311), explain how the program on page 317 that passed an ifstream to the Sales_data read function works.

 

 

Key Concept: Conversions among Types Related by Inheritance

There are three things that are important to understand about conversions among classes related by inheritance:

 

• The conversion from derived to base applies only to pointer or reference types.

 

• There is no implicit conversion from the base-class type to the derived type.

 

• Like any member, the derived-to-base conversion may be inaccessible due to access controls. We’ll cover accessibility in §15.5 (p. 613).

 

Although the automatic conversion applies only to pointers and references, most classes in an inheritance hierarchy (implicitly or explicitly) define the copy-control members (Chapter 13). As a result, we can often copy, move, or assign an object of derived type to a base-type object. However, copying, moving, or assigning a derived-type object to a base-type object copies, moves, or assigns only the members in the base-class part of the object.

 

 

Calls to Virtual Functions May Be Resolved at Run Time

 

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.

 

 

Virtual Functions in a Derived Class

 

When a derived class overrides a virtual function, it may, but is not required to, repeat the virtual keyword. Once a function is declared as virtual, it remains virtual in all the derived classes.

 

A derived-class function that overrides an inherited virtual function must have exactly the same parameter type(s) as the base-class function that it overrides.

 

With one exception, the return type of a virtual in the derived class also must match the return type of the function from the base class. The exception applies to virtuals that return a reference (or pointer) to types that are themselves related by inheritance. That is, if D is derived from B, then a base class virtual can return a B* and the version in the derived can return a D*. However, such return types require that the derived-to-base conversion from D to B is accessible. §15.5 (p. 613) covers how to determine whether a base class is accessible. We’ll see an example of this kind of virtual function in §15.8.1 (p. 633).

 

Image Note

A function that is virtual in a base class is implicitly virtual in its derived classes. When a derived class overrides a virtual, the parameters in the base and derived classes must match exactly.

 

 

The final and override Specifiers

 

As we’ll see in §15.6 (p. 620), it is legal for a derived class to define a function with the same name as a virtual in its base class but with a different parameter list. The compiler considers such a function to be independent from the base-class function. In such cases, the derived version does not override the version in the base class. In practice, such declarations often are a mistake—the class author intended to override a virtual from the base class but made a mistake in specifying the parameter list.

 
Image

Finding such bugs can be surprisingly hard. Under the new standard we can specify override on a virtual function in a derived class. Doing so makes our intention clear and (more importantly) enlists the compiler in finding such problems for us. The compiler will reject a program if a function marked override does not override an existing virtual function:

 

 

struct B {
    virtual void f1(int) const;
    virtual void f2();
    void f3();
};
struct D1 : B {
    void f1(int) const override; // ok: f1 matches f1 in the base
    void f2(int) override; // error: B has no f2(int) function
    void f3() override;    // error: f3 not virtual
    void f4() override;    // error: B doesn't have a function named f4
};

 

In D1, the override specifier on f1 is fine; both the base and derived versions of f1 are const members that take an int and return void. The version of f1 in D1 properly overrides the virtual that it inherits from B.

 

The declaration of f2 in D1 does not match the declaration of f2 in B—the version defined in B takes no arguments and the one defined in D1 takes an int. Because the declarations don’t match, f2 in D1 doesn’t override f2 from B; it is a new function that happens to have the same name. Because we said we intended this declaration to be an override and it isn’t, the compiler will generate an error.

 

Because only a virtual function can be overridden, the compiler will also reject f3 in D1. That function is not virtual in B, so there is no function to override. Similarly f4 is in error because B doesn’t even have a function named f4.

 

We can also designate a function as final. Any attempt to override a function that has been defined as final will be flagged as an error:

 

 

struct D2 : B {
    // inherits f2() and f3() from B and overrides f1(int)
    void f1(int) const final; // subsequent classes can't override f1 (int)
};
struct D3 : D2 {
    void f2();          // ok: overrides f2 inherited from the indirect base, B
    void f1(int) const; // error: D2 declared f2 as final
};

 

final and override specifiers appear after the parameter list (including any const or reference qualifiers) and after a trailing return (§ 6.3.3, p. 229).

 

Virtual Functions and Default Arguments

 

Like any other function, a virtual function can have default arguments (§ 6.5.1, p. 236). If a call uses a default argument, the value that is used is the one defined by the static type through which the function is called.

 

That is, when a call is made through a reference or pointer to base, the default argument(s) will be those defined in the base class. The base-class arguments will be used even when the derived version of the function is run. In this case, the derived function will be passed the default arguments defined for the base-class version of the function. If the derived function relies on being passed different arguments, the program will not execute as expected.

 

Image Best Practices

Virtual functions that have default arguments should use the same argument values in the base and derived classes.

 

 

Circumventing the Virtual Mechanism

 

In some cases, we want to prevent dynamic binding of a call to a virtual function; we want to force the call to use a particular version of that virtual. We can use the scope operator to do so. For example, this code:

 

 

//  calls the version from the base class regardless of the dynamic type of baseP
double undiscounted = baseP->Quote::net_price(42);

 

calls the Quote version of net_price regardless of the type of the object to which baseP actually points. This call will be resolved at compile time.

 

Image Note

Ordinarily, only code inside member functions (or friends) should need to use the scope operator to circumvent the virtual mechanism.

 

 

Why might we wish to circumvent the virtual mechanism? The most common reason is when a derived-class virtual function calls the version from the base class. In such cases, the base-class version might do work common to all types in the hierarchy. The versions defined in the derived classes would do whatever additional work is particular to their own type.

 

Image Warning

If a derived virtual function that intended to call its base-class version omits the scope operator, the call will be resolved at run time as a call to the derived version itself, resulting in an infinite recursion.

 

 

Exercises Section 15.3

 

Exercise 15.11: Add a virtual debug function to your Quote class hierarchy that displays the data members of the respective classes.

Exercise 15.12: Is it ever useful to declare a member function as both override and final? Why or why not?

Exercise 15.13: Given the following classes, explain each print function:

 

class base {
public:
   string name() { return basename; }
   virtual void print(ostream &os) { os << basename; }
private:
   string basename;
};
class derived : public base {
public:
   void print(ostream &os) { print(os); os << " " << i; }
private:
   int i;
};

 

If there is a problem in this code, how would you fix it?

 

Exercise 15.14: Given the classes from the previous exercise and the following objects, determine which function is called at run time:

 

base bobj;     base *bp1 = &bobj;   base &br1 = bobj;
derived dobj;  base *bp2 = &dobj;   base &br2 = dobj;

 

(a) bobj.print();

 

(b) dobj.print();

 

(c) bp1->name();

 

(d) bp2->name();

 

(e) br1.print();

 

(f) br2.print();

 

 
Team LiB
Previous Section Next Section