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 visualizing 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? A Shape is too general or ambiguous - there are many different shapes - for us to draw or program.

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

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

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

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

Triangle
-----------------
-----------------
+draw() : void
+erase() : 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: Creating an abstract class. Pure virtual functions do not have a body. Instead, we add the pure specifier, = 0, to the end of the prototype. If the Shape draw function doesn't have a body, then we don't need an algorithm to draw it, solving our problem. Making Shape draw a pure virtual function makes 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 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. The code instantiates a Shape subclass and upcasts it to a Shape pointer, s, and passes it to a function. The Shape class needs the pure virtual function 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, making a draw function in the Shape class necessary. The next section details how the compiler polymorphically locates the correct function.
  2. Potentially, pointer s can point to a Circle, Rectangle, or Triangle object, but the program can't determine the exact shape at compile-time. It only determines the exact shape when the render function calls the draw function, which is long after the compiler finishes code generation. What guarantee 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, guaranteeing 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 the 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 who receive a salary by dividing their annual salary by the number of pay periods. If the employees are paid twice per month, then their current pay is salary / 24, where 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

Pure virtual Functions With Bodies

The previous example demonstrated a virtual function, SalesEmployee::calc_pay(), chaining to the function it overrides, SalariedEmployee::calc_pay(). However, SalariedEmployee::calc_pay() cannot chain to Employee::calc_pay() because it doesn't have a body. C++ allows pure virtual functions to have executable bodies but requires programmers to separate the pure virtual specification from the body definition.

The UML Shape class diagram or hierarchy with four classes: Shape, Circle, Rectangle, and Triangle. Each class has a draw function, but none have member variables. The UML diagram shows the Shape class name and its draw function in italics, indicating that they are abstract.
class Shape
{
    public:
        virtual void draw() = 0;	// (a)
};

void Shape::draw()			// (b)
{
    { cout << "Shape - "; }
}
class Circle : public Shape
{
    public:
        virtual void draw()
        {
            Shape::draw();		// (c)
            cout << "Circle\n";
        }
};
class Rectangle : public Shape
{
    public:
        virtual void draw()
        {
            Shape::draw();		// (c)
            cout << "Rectangle\n";
        }
};
class Triangle : public Shape
{
    public:
        virtual void draw()
        {
            Shape::draw();		// (c)
            cout << "Triangle\n";
        }
};
Chaining to pure virtual functions.
  1. The Shape class has a prototype for a virtual draw function with a pure specifier, making it a pure virtual function, and the Shape class abstract.
  2. The C++ syntax allows pure virtual functions to have a body but it must be defined outside the class in a source code (i.e., .cpp) file. Although the draw function has a body, it is still pure virtual, so the Shape subclasses must override it.
  3. The Shape subclass draw functions can chain to the superclass function because it has a body.
The complete Shape.cpp program, including main.

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