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