12.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 toString 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 toString on the BasePlusCommissionEmployee object, even though the pointer was aimed at a BasePlusCommissionEmployee object that has its own custom toString function.

12.4.1 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.

Software Engineering Observation 12.3

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

12.4.2 Declaring virtual Functions

To enable this behavior, we declare draw in the base class as a virtualfunction, 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 do 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.

Software Engineering Observation 12.4

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.

 

Good Programming Practice 12.1

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

 

Software Engineering Observation 12.5

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.

12.4.3 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 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.

12.4.4 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. Dynamic binding with virtual functions occurs only off pointers and references.

12.4.5 virtual Functions in the CommissionEmployee Hierarchy

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’ve modified these to declare each class’s earnings and toString member functions as virtual (lines 28–29 of Fig. 12.4 and lines 17–18 of Fig. 12.5). Because functions earnings and toString are virtual in class CommissionEmployee, class BasePlusCommissionEmployee’s earnings and toString functions override class CommissionEmployee’s. In addition, class BasePlusCommissionEmployee’s earnings and toString functions are declared with C++11’s override keyword.

Error-Prevention Tip 12.1

To help prevent errors, apply C++11’s override keyword to the prototype of every derived-class function that overrides a base-class virtual function. This enables the compiler to check whether the base class has a virtual member function with the same signature. If not, the compiler generates an error. Not only does this ensure that you override the base-class function with the appropriate signature, it also prevents you from accidentally hiding a base-class function that has the same name and a different signature.

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 toString, the BasePlusCommissionEmployee object’s corresponding function will be invoked polymorphically. 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.

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

Alternate View

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

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

Alternate View

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

We modified Fig. 12.1 to create the program of Fig. 12.6. Lines 32–44 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 47 aims the base-class pointer commissionEmployeePtr at derived-class object basePlusCommissionEmployee. When line 54 invokes member function toString off the base-class pointer, the derived-class BasePlusCommissionEmployee’s toString member function is invoked, so line 54 outputs different text than line 46 does in Fig. 12.1 (when member function toString 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. When commissionEmployeePtr points to a CommissionEmployee object, class CommissionEmployee’s toString function is invoked (Fig. 12.6, line 36), and when commissionEmployeePtr points to a BasePlusCommissionEmployee object, class BasePlusCommissionEmployee’s toString function is invoked (line 54). Thus, the same toString message—sent off a base-class pointer to a variety of derived-class objects—takes on many forms. This is polymorphic behavior.



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

Alternate View

 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      // create base-class object
11      CommissionEmployee commissionEmployee{
12         "Sue" , "Jones", "222-22-2222", 10000, .06};
13
14      // create derived-class object
15      BasePlusCommissionEmployee basePlusCommissionEmployee{
16         "Bob" , "Lewis", "333-33-3333", 5000, .04, 300};
17
18      cout << fixed << setprecision(2); // set floating-point formatting
19
20      // output objects using static binding
21      cout << "INVOKING TOSTRING FUNCTION ON BASE-CLASS AND DERIVED-CLASS "
22         << "
OBJECTS WITH STATIC BINDING
"
23         << commissionEmployee.toString() // static binding
24         << "

"
25         << basePlusCommissionEmployee.toString(); // static binding
26
27      // output objects using dynamic binding
28      cout << "

INVOKING TOSTRING FUNCTION ON BASE-CLASS AND "
29         << "
DERIVED-CLASS OBJECTS WITH DYNAMIC BINDING" ;
30
31      // natural: aim base-class pointer at base-class object              
32      const CommissionEmployee* commissionEmployeePtr{&commissionEmployee};
33      cout << "

CALLING VIRTUAL FUNCTION TOSTRING WITH BASE-CLASS POINTER"
34         << "
TO BASE-CLASS OBJECT INVOKES BASE-CLASS "
35         << "TOSTRING FUNCTION:
"
36         << commissionEmployeePtr->toString(); // base version
37
38      // natural: aim derived-class pointer at derived-class object   
39      const BasePlusCommissionEmployee* basePlusCommissionEmployeePtr{
40         &basePlusCommissionEmployee}; // natural                        
41      cout << "

CALLING VIRTUAL FUNCTION TOSTRING WITH DERIVED-CLASS "
42         << "POINTER
TO DERIVED-CLASS OBJECT INVOKES DERIVED-CLASS "
43         << "TOSTRING FUNCTION:
"
44         << basePlusCommissionEmployeePtr->toString(); // derived version 
45
46      // aim base-class pointer at derived-class object   
47      commissionEmployeePtr = &basePlusCommissionEmployee;
48      cout << "

CALLING VIRTUAL FUNCTION TOSTRING WITH BASE-CLASS POINTER"
49         << "
TO DERIVED-CLASS OBJECT INVOKES DERIVED-CLASS "
50         << "TOSTRING FUNCTION:
" ;
51
52      // polymorphism; invokes BasePlusCommissionEmployee's toString
53      // via base-class pointer to derived-class object 
54      cout<< commissionEmployeePtr->toString() << endl;
55   }

INVOKING TOSTRING FUNCTION ON BASE-CLASS AND DERIVED-CLASS
OBJECTS WITH STATIC BINDING
commission employee: Sue Jones
social security number: 222-22-2222
gross sales: 10000.00
commission rate: 0.06

base-salaried commission employee: Bob Lewis
social security number: 333-33-3333
gross sales: 5000.00
commission rate: 0.04
base salary: 300

INVOKING TOSTRING FUNCTION ON BASE-CLASS AND
DERIVED-CLASS OBJECTS WITH DYNAMIC BINDING

CALLING VIRTUAL FUNCTION TOSTRING WITH BASE-CLASS POINTER
TO BASE-CLASS OBJECT INVOKES BASE-CLASS TOSTRING FUNCTION:
commission employee: Sue Jones
social security number: 222-22-2222
gross sales: 10000.00
commission rate: 0.06

CALLING VIRTUAL FUNCTION TOSTRING WITH DERIVED-CLASS POINTER
TO DERIVED-CLASS OBJECT INVOKES DERIVED-CLASS TOSTRING FUNCTION:
base-salaried commission employee: Bob Lewis
social security number: 333-33-3333
gross sales: 5000.00
commission rate: 0.04
base salary: 300

CALLING VIRTUAL FUNCTION TOSTRING WITH BASE-CLASS POINTER
TO DERIVED-CLASS OBJECT INVOKES DERIVED-CLASS TOSTRING FUNCTION:
base-salaried commission employee: Bob Lewis
social security number: 333-33-3333
gross sales: 5000.00
commission rate: 0.04
base salary: 300

12.4.6 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. For example, in class CommissionEmployee’s definition (Fig. 12.4), 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 and requires a destructor.

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.

 

Common Programming Error 12.1

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

The preceding destructor definition also may be written as follows:

virtual ~CommissionEmployee() = default;

In C++11, you can tell the compiler to explicitly generate the default version of a default constructor, copy constructor, move constructor, copy assignment operator, move assignment operator or destructor by following the special member function’s prototype with = default. This is useful, for example, when you explicitly define a constructor for a class and still want the compiler to generate a default constructor as well—in that case, add the following declaration to your class definition:

ClassName() = default;

12.4.7 C++11: final Member Functions and Classes

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