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.
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.
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:
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.
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.
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.
The Employee calc_pay function is a pure virtual function, making the function and the Employee class abstract
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
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.
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.
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.
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.
The Shape subclass draw functions can chain to the superclass function because it has a body.
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:
be a superclass
have concrete features (both variables and functions) that can be inherited by subclasses