Abstract Classes and C++ - dummies

By Stephen R. Davis

C++ supports late binding, which is when it resolves a method call based on the run-time type (or dynamic type) of the target object rather than its declared type (or static type). This is demonstrated in the following C++ code snippet:

#include <iostream>
using namespace std;
class Oven
{
  public:
    virtual void cook()
    {
        cout << "Cooking with an oven" << endl;
    }
};
class MicrowaveOven: public Oven
{
  public:
    virtual void cook()
    {
        cout << "Cooking with a microwave oven" << endl;
    }
};
void prepareMeal(Oven& oven)
{
    oven.cook();
}

In the function prepareMeal(), the call to oven.cook() may pass to Oven::cook() or MicrowaveOven::cook() depending upon the run time (the “actual”) type of the oven object passed.

The virtual keyword is critical here. Without it, the cook() method would be bound early, based on the compile-time type, and invoke Oven::cook() every time. Once declared virtual in the Oven class, the method is assumed to be virtual in every subclass but it doesn’t hurt to repeat the declaration so that readers understand.

The following simple program demonstrates this principle in practice:

int main()
{
    Oven oven;
    prepareMeal(oven);
    MicrowaveOven mo;
    prepareMeal(mo);
    return 0;
}

In this program, the call to cook() generates two different outputs depending upon the type of oven:

Cooking with an oven
Cooking with a microwave oven

It is not always the case, that a method in the base class can be defined. Consider the Oven case more carefully. There are a number of different types of ovens — conventional ovens, convection ovens, and microwave ovens — but one could argue that there is no actual oven that doesn’t belong to one of these subclasses. You may be able to say how the various types of ovens perform the cook operation — that is, what a ConventionalOven::cook() and a MicrowaveOven::cook() should do can be defined. It’s probably not possible to define what actions Oven::cook() should perform.

You cannot simply leave Oven::cook() undeclared in a strongly typed language like C++. However, you can declare a method but leave it unimplemented if no implementation exists. One uses the following curious syntax to do so:

class Oven
{
  public:
    virtual void cook() = 0;
};

This code declares a method Oven::cook() that is bound late but does not implement the method. In fact, it goes further by saying that the method will not be implemented. In C++, such a method is said to be pure virtual. C++ programmers also use the term preferred in many other strongly typed computer languages: abstract. The Oven class is said to be abstract.

An abstract represents a property that you know the class possesses but do not know how to implement unambiguously in the current class.

A class is abstract if it contains one or more pure virtual methods. The significance of this is that you cannot instantiate an abstract class. Thus the following is no longer allowed:

int main()
{
    Oven oven;
    prepareMeal(oven);
    return 0;
}

The reason for this is quite simple: if you created an object of class Oven and then tried to invoke oven.cook(), what should the compiler do?

At a more philosophical level, it’s fine to say that there is some common term called Oven that describes conventional ovens and microwave ovens and convection ovens. This term Oven is a usual concept because it binds up the similarities in all of these subclasses. But there is no instance of an oven that isn’t one of the subclasses of Oven.

A subclass of an abstract class is itself abstract until all pure virtual methods have been overridden by non-abstract (that is, concrete) versions. Thus the class MicrowaveOven in the previous code snippet is not abstract — even if Oven were abstract — because it overrides cook() with its own concrete version.

Notice that there is nothing wrong with the function prepareMeal() defined as follows:

void prepareMeal(Oven& oven)
{
    oven.cook();
}

Even though the argument is declared to be an Oven, it can only be invoked with some subclass of Oven, such as MicrowaveOven or ConventionalOven, for which cook() is defined.