12.5. Pure Virtual Functions and Abstract Classes

Time: 00:05:08 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)

We began the chapter with three shape classes: Circle, Rectangle, and Triangle classes. Although we didn't write the code to draw the shapes, we can imagine, at least generally, how the shapes look. Given some specific information (e.g., a radius, width, or height), we could develop algorithms and write code to draw these shapes. Later, as one step toward implementing polymorphism, we created an inheritance hierarchy by adding a Shape class and making it a superclass of the other shapes. Imagine that we want to visualize a Shape. How does it look? Does the Shape you visualize look like the Shape I visualize? More precisely, what code would we write for the Shape class's draw function? Clearly, a Shape is too general or ambiguous - there are many different shapes - for us to draw or program.

The UML Shape class diagram or hierarcy consisting of four classes: Shape, Circle, Rectangle, and Triangle. None of the classes show member variables.

Shape
-----------------
-----------------
+draw() : void

Circle
-----------------
-----------------
+draw() : void

Rectangle
-----------------
-----------------
+draw() : void

Triangle
-----------------
-----------------
+draw() : void
UML class diagram of the Shape classes. A Shape is too abstract to draw. Alternatively, a Circle, a Rectangle, or a Triangle are concrete and drawable. The draw function of each subclass overrides the Shape class's draw function.

Pure Virtual Functions

"Normal" functions, including virtual functions, must have a body (defined inline in the class or defined in an appropriate .cpp file). In the case of our shape classes, there must be algorithms describing how to draw each shape, and there must be code in the body of each shape draw function. But the Shape draw function presents us with a problem: it's too ambiguous to draw, so we can't write code in its draw function. But if we remove the draw function from the Shape class, we lose one requirement for polymorphism: overridden functions. We solve this problem by making the Shape draw function a pure virtual function.

class Shape
{
	private:
		...
	public:
		...
		virtual void draw() = 0;
};
The Shape draw function. Pure virtual functions do not have a body. Instead, we add = 0 at the end of the prototype. If the Shape draw function doesn't have a body, then we don't need an algorithm to drawing it, solving our problem. Making Shape draw a pure virtual function makes the Shape an abstract class. Indeed, a C++ pure virtual function is, in object-speak, an abstract function. The distinction between an abstract and a concrete class, the kind that we have been building throughout the semester, is that we can instantiate a concrete class but not an abstract one.

We might well ask, "If a function doesn't have a body, what good is it?" We can answer that question in two ways - ways that might seem different but are just other aspects of how C++ implements polymorphism.

switch (choice)
{
	case 'c' :
		s = new Circle();
		break;
	case 's' :
		s = new Rectangle();
		break;
	case 't' :
		s = new Triangle();
		break;
}
void render(Shape* s)
{
	if (s != nullptr)
		s->draw();
	else
		throw "Null pointer error";
}
 
 
 
 
 
The role of pure virtual functions. One Shape subclass is instantiated and upcast to a Shape pointer, s, and passed to a function. The Shape pure virtual function needed for two reasons:
  1. The compiler will search the symbol table for the draw function beginning with the Shape entry. Compilation fails if it can't find a draw function. The next section provides more detail about how the compiler polymorphically locates the correct function.
  2. Potentially, pointer s can point to a Circle, Rectangle, or Triangle object, but the exact object is not determined at compile-time. The exact object is only determined when a call is made to the draw function, long after the compiler is done. What garentee does the compiler have that the object s points to has a draw function? To become a concrete class, subclasses must override all pure virtual functions in their superclass, garenteeing that s->draw(); will find a draw function.

We can force subclasses to have a particular function by including an appropriate pure virtual function in their superclass. A more extensive example will help us better understand the value of abstract functions.

UML class diagram of three classes:

Employee (in italics)
-----------------
-----------------
+calc_pay() = 0 : double (in italics)

SalariedEmployee
-----------------
-salary : double
-----------------
+calc_pay() : double

SalesEmployee
-----------------
-commission : double
-----------------
+calc_pay() : double

SalesEmployee is a subclass of SalariedEmployee, and SalariedEmployee is a subclass of Employee.
Virtual functions in an inheritance hierarchy. UML class diagrams indicate abstract classes and operations with italic characters. In this example, Employee and its calc_pay function are abstract. The subclasses override the abstract (pure virtual) function with concrete functions, becoming concrete classes.
EmployeeSalariedEmployeeSalesEmployee
public:
    virtual double calc_pay() = 0;




public:
    double calc_pay()
    {
        return salary / 24;
    }

public:
    double calc_pay()
    {
        return SalariedEmployee::calc_pay()
            + commission;
    }
(a)(b)(c)
Chaining overridden functions. Previously, we saw that it was possible to chain function calls together, which allows us to use, if not to directly access, private variables in the classes throughout the inheritance hierarchy. Having a pure virtual function in a superclass does not prevent function chaining.
  1. The Employee calc_pay function is a pure virtual function, making the function and the Employee class abstract
  2. We calculate the pay for employees that receive a salary by taking their annual salary and dividing it by the number of pay periods. If the employees are paid twice per month, then their current pay is salary / 24 - salary is a private member of the SalariedEmployee class
  3. The current pay for sales employees is their salary plus a commission. Notice:
    • salary is a private member of the SalariedEmployee class and so is not directly accessible from a SalesEmployee function
    • The SalesEmployee calc_pay function calls the SalariedEmployee calc_pay function, which makes it unnecessary for a SalesEmployee object to directly access salary
    • The notation SalariedEmployee:: is necessary to specify which calc_pay function to call. Without this notation, the compiler will create an erroneous recursive call to the SalesEmployee calc_pay function

Abstract Classes

Classes that have one or more pure virtual functions are said to be abstract. Until this section, all our class examples have been concrete. The difference between concrete and abstract classes is that a program may make an instance of a concrete class but not an abstract class. If a program can't have an instance of an abstract class, what good is the class? Although a program can't make an object from an abstract class, there are some things that an abstract class can still do:

  1. be a superclass
  2. have concrete features (both variables and functions) that can be inherited by subclasses
  3. participate in (i.e., be the target of) an upcast
  4. participate in polymorphism