12.3.4. Virtual Functions and Virtual Destructors

In Section 12.3.1, we aimed a base-class CommissionEmployee pointer at a derived-class BasePlusCommissionEmployee object, then invoked member function print through that pointer. Recall that the type of the handle determined which class’s functionality to invoke. In that case, the CommissionEmployee pointer invoked the CommissionEmployee member function print on the BasePlusCommissionEmployee object, even though the pointer was aimed at a BasePlusCommissionEmployee object that has its own custom print function.


Image Software Engineering Observation 12.4

With virtual functions, the type of the object, not the type of the handle used to invoke the member function, determines which version of a virtual function to invoke.


Why virtual Functions Are Useful

First, we consider why virtual functions are useful. Suppose that shape classes such as Circle, Triangle, Rectangle and Square are all derived from base class Shape. Each of these classes might be endowed with the ability to draw itself via a member function draw, but the function for each shape is quite different. In a program that draws a set of shapes, it would be useful to be able to treat all the shapes generally as objects of the base class Shape. Then, to draw any shape, we could simply use a base-class Shape pointer to invoke function draw and let the program determine dynamically (i.e., at runtime) which derived-class draw function to use, based on the type of the object to which the base-class Shape pointer points at any given time. This is polymorphic behavior.

Declaring virtual Functions

To enable this behavior, we declare draw in the base class as a virtual function, and we override draw in each of the derived classes to draw the appropriate shape. From an implementation perspective, overriding a function is no different than redefining one (which is the approach we’ve been using until now). An overridden function in a derived class has the same signature and return type (i.e., prototype) as the function it overrides in its base class. If we do not declare the base-class function as virtual, we can redefine that function. By contrast, if we declare the base-class function as virtual, we can override that function to enable polymorphic behavior. We declare a virtual function by preceding the function’s prototype with the keyword virtual in the base class. For example,

virtual void draw() const;

would appear in base class Shape. The preceding prototype declares that function draw is a virtual function that takes no arguments and returns nothing. This function is declared const because a draw function typically would not make changes to the Shape object on which it’s invoked—virtual functions do not have to be const functions.


Image Software Engineering Observation 12.5

Once a function is declared virtual, it remains virtual all the way down the inheritance hierarchy from that point, even if that function is not explicitly declared virtual when a derived class overrides it.



Image Good Programming Practice 12.1

Even though certain functions are implicitly virtual because of a declaration made higher in the class hierarchy, explicitly declare these functions virtual at every level of the class hierarchy to promote program clarity.



Image Software Engineering Observation 12.6

When a derived class chooses not to override a virtual function from its base class, the derived class simply inherits its base class’s virtual function implementation.


Invoking a virtual Function Through a Base-Class Pointer or Reference

If a program invokes a virtual function through a base-class pointer to a derived-class object (e.g., shapePtr->draw()) or a base-class reference to a derived-class object (e.g., shapeRef.draw()), the program will choose the correct derived-class draw function dynamically (i.e., at execution time) based on the object type—not the pointer or reference type. Choosing the appropriate function to call at execution time (rather than at compile time) is known as dynamic binding or late binding.

Invoking a virtual Function Through an Object’s Name

When a virtual function is called by referencing a specific object by name and using the dot member-selection operator (e.g., squareObject.draw()), the function invocation is resolved at compile time (this is called static binding) and the virtual function that’s called is the one defined for (or inherited by) the class of that particular object—this is not polymorphic behavior. Thus, dynamic binding with virtual functions occurs only off pointers (and, as we’ll soon see, references).

virtual Functions in the CommissionEmployee Hierarchy
Image

Now let’s see how virtual functions can enable polymorphic behavior in our employee hierarchy. Figures 12.412.5 are the headers for classes CommissionEmployee and BasePlusCommissionEmployee, respectively. We modified these to declare each class’s earnings and print member functions as virtual (lines 29–30 of Fig. 12.4 and lines 19–20 of Fig. 12.5). Because functions earnings and print are virtual in class CommissionEmployee, class BasePlusCommissionEmployee’s earnings and print functions override class CommissionEmployee’s. In addition, class BasePlusCommissionEmployee’s earnings and print functions are declared override.


 1   // Fig. 12.4: CommissionEmployee.h
 2   // CommissionEmployee class header declares earnings and print as virtual.
 3   #ifndef COMMISSION_H
 4   #define COMMISSION_H
 5
 6   #include <string> // C++ standard string class
 7
 8   class CommissionEmployee
 9   {
10   public:
11      CommissionEmployee( const std::string &, const std::string &,
12         const std::string &, double = 0.0, double = 0.0 );
13
14      void setFirstName( const std::string & ); // set first name
15      std::string getFirstName() const; // return first name
16
17      void setLastName( const std::string & ); // set last name
18      std::string getLastName() const; // return last name
19
20      void setSocialSecurityNumber( const std::string & ); // set SSN
21      std::string getSocialSecurityNumber() const; // return SSN
22
23      void setGrossSales( double ); // set gross sales amount
24      double getGrossSales() const; // return gross sales amount
25
26      void setCommissionRate( double ); // set commission rate
27      double getCommissionRate() const; // return commission rate
28
29      virtual double earnings() const; // calculate earnings
30      virtual void print() const; // print object           
31   private:
32      std::string firstName;
33      std::string lastName;
34      std::string socialSecurityNumber;
35      double grossSales; // gross weekly sales
36      double commissionRate; // commission percentage
37   }; // end class CommissionEmployee
38
39   #endif


Fig. 12.4. CommissionEmployee class header declares earnings and print as virtual.


 1   // Fig. 12.5: BasePlusCommissionEmployee.h
 2   // BasePlusCommissionEmployee class derived from class
 3   // CommissionEmployee.
 4   #ifndef BASEPLUS_H
 5   #define BASEPLUS_H
 6
 7   #include <string> // C++ standard string class
 8   #include "CommissionEmployee.h" // CommissionEmployee class declaration
 9
10   class BasePlusCommissionEmployee : public CommissionEmployee
11   {
12   public:
13      BasePlusCommissionEmployee( const std::string &, const std::string &,
14         const std::string &, double = 0.0, double = 0.0, double = 0.0 );
15
16      void setBaseSalary( double ); // set base salary
17      double getBaseSalary() const; // return base salary
18
19      virtual double earnings() const override; // calculate earnings
20      virtual void print() const override; // print object           
21   private:
22      double baseSalary; // base salary
23   }; // end class BasePlusCommissionEmployee
24
25   #endif


Fig. 12.5. BasePlusCommissionEmployee class header declares earnings and print functions as virtual and override.


Image Error-Prevention Tip 12.1

Image

Apply C++11’s override keyword to every overridden function in a derived-class. This forces the compiler to check whether the base class has a member function with the same name and parameter list (i.e., the same signature). If not, the compiler generates an error.


Now, if we aim a base-class CommissionEmployee pointer at a derived-class BasePlusCommissionEmployee object, and the program uses that pointer to call either function earnings or print, the BasePlusCommissionEmployee object’s corresponding function will be invoked. There were no changes to the member-function implementations of classes CommissionEmployee and BasePlusCommissionEmployee, so we reuse the versions of Figs. 11.14 and 11.15.

We modified Fig. 12.1 to create the program of Fig. 12.6. Lines 40–51 of Fig. 12.6 demonstrate again that a CommissionEmployee pointer aimed at a CommissionEmployee object can be used to invoke CommissionEmployee functionality, and a BasePlusCommissionEmployee pointer aimed at a BasePlusCommissionEmployee object can be used to invoke BasePlusCommissionEmployee functionality. Line 54 aims the base-class pointer commissionEmployeePtr at derived-class object basePlusCommissionEmployee. Note that when line 61 invokes member function print off the base-class pointer, the derived-class BasePlusCommissionEmployee’s print member function is invoked, so line 61 outputs different text than line 53 does in Fig. 12.1 (when member function print was not declared virtual). We see that declaring a member function virtual causes the program to dynamically determine which function to invoke based on the type of object to which the handle points, rather than on the type of the handle. Note again that when commissionEmployeePtr points to a CommissionEmployee object, class CommissionEmployee’s print function is invoked (Fig. 12.6, line 40), and when CommissionEmployeePtr points to a BasePlusCommissionEmployee object, class BasePlusCommissionEmployee’s print function is invoked (line 61). Thus, the same message—print, in this case—sent (off a base-class pointer) to a variety of objects related by inheritance to that base class, takes on many forms—this is polymorphic behavior.


 1   // Fig. 12.6: fig12_06.cpp
 2   // Introducing polymorphism, virtual functions and dynamic binding.
 3   #include <iostream>
 4   #include <iomanip>
 5   #include "CommissionEmployee.h"
 6   #include "BasePlusCommissionEmployee.h"
 7   using namespace std;
 8
 9   int main()
10   {
11      // create base-class object
12      CommissionEmployee commissionEmployee(
13         "Sue", "Jones", "222-22-2222", 10000, .06 );
14
15      // create base-class pointer
16      CommissionEmployee *commissionEmployeePtr = nullptr;
17
18      // create derived-class object
19      BasePlusCommissionEmployee basePlusCommissionEmployee(
20         "Bob", "Lewis", "333-33-3333", 5000, .04, 300 );
21
22      // create derived-class pointer
23      BasePlusCommissionEmployee *basePlusCommissionEmployeePtr = nullptr;
24
25      // set floating-point output formatting
26      cout << fixed << setprecision( 2 );
27
28      // output objects using static binding
29      cout << "Invoking print function on base-class and derived-class "
30         << " objects with static binding ";
31      commissionEmployee.print(); // static binding
32      cout << " ";
33      basePlusCommissionEmployee.print(); // static binding
34
35      // output objects using dynamic binding
36      cout << " Invoking print function on base-class and "
37         << "derived-class objects with dynamic binding";
38
39      // aim base-class pointer at base-class object and print
40      commissionEmployeePtr = &commissionEmployee;            
41      cout << " Calling virtual function print with base-class pointer"
42         << " to base-class object invokes base-class "
43         << "print function: ";
44      commissionEmployeePtr->print(); // invokes base-class print
45
46      // aim derived-class pointer at derived-class object and print
47      basePlusCommissionEmployeePtr = &basePlusCommissionEmployee;  
48      cout << " Calling virtual function print with derived-class "
49         << "pointer to derived-class object invokes derived-class "
50         << "print function: ";
51      basePlusCommissionEmployeePtr->print(); // invokes derived-class print
52
53      // aim base-class pointer at derived-class object and print
54      commissionEmployeePtr = &basePlusCommissionEmployee;       
55      cout << " Calling virtual function print with base-class pointer"
56         << " to derived-class object invokes derived-class "
57         << "print function: ";
58
59      // polymorphism; invokes BasePlusCommissionEmployee's print;
60      // base-class pointer to derived-class object               
61      commissionEmployeePtr->print();                             
62      cout << endl;
63   } // end main

Image

Fig. 12.6. Demonstrating polymorphism by invoking a derived-class virtual function via a base-class pointer to a derived-class object.

virtual Destructors

A problem can occur when using polymorphism to process dynamically allocated objects of a class hierarchy. So far you’ve seen destructors that are not declared with keyword virtual. If a derived-class object with a non-virtual destructor is destroyed by applying the delete operator to a base-class pointer to the object, the C++ standard specifies that the behavior is undefined.

The simple solution to this problem is to create a public virtual destructor in the base class. If a base class destructor is declared virtual, the destructors of any derived classes are also virtual and they override the base class destructor. For example, in class CommissionEmployee’s definition, we can define the virtual destructor as follows:

virtual ~CommissionEmployee() { }

Now, if an object in the hierarchy is destroyed explicitly by applying the delete operator to a base-class pointer, the destructor for the appropriate class is called based on the object to which the base-class pointer points. Remember, when a derived-class object is destroyed, the base-class part of the derived-class object is also destroyed, so it’s important for the destructors of both the derived and base classes to execute. The base-class destructor automatically executes after the derived-class destructor. From this point forward, we’ll include a virtual destructor in every class that contains virtual functions.


Image Error-Prevention Tip 12.2

If a class has virtual functions, always provide a virtual destructor, even if one is not required for the class. This ensures that a custom derived-class destructor (if there is one) will be invoked when a derived-class object is deleted via a base class pointer.



Image Common Programming Error 12.1

Constructors cannot be virtual. Declaring a constructor virtual is a compilation error.


C++11: final Member Functions and Classes
Image

Prior to C++11, a derived class could override any of its base class’s virtual functions. In C++11, a base-class virtual function that’s declared final in its prototype, as in

virtual someFunction( parameters ) final;

cannot be overridden in any derived class—this guarantees that the base class’s final member function definition will be used by all base-class objects and by all objects of the base class’s direct and indirect derived classes. Similarly, prior to C++11, any existing class could be used as a base class in a hierarchy. As of C++11, you can declare a class as final to prevent it from being used as a base class, as in

class MyClass final // this class cannot be a base class
{
   // class body
};

Attempting to override a final member function or inherit from a final base class results in a compilation error.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset