Manufacturing Abstract Bases



Item 33. Manufacturing Abstract Bases

Abstract base classes typically represent abstract concepts from the problem domain, and therefore it doesn't make sense to declare objects of those types. We make a base class abstract by declaring (or inheriting) at least one pure virtual function, and the compiler will then ensure that no objects of the abstract base class can be created.

class ABC {
  public:
    virtual ~ABC();
    virtual void anOperation() = 0; // pure
    //...
};

However, there may be cases where we have no reasonable candidate for a pure virtual function but still want the class to act like an abstract base. In those cases, we can approximate the nature of an abstract class by making sure that there are no public constructors in the class. This invariably means we must explicitly declare at least one constructor, since otherwise the compiler will implicitly declare a public inline default constructor. Since the compiler will also declare an implicit copy constructor if we don't declare one explicitly, we typically must declare two constructors.

class ABC {
  public:
    virtual ~ABC();
  protected:
    ABC();
    ABC( const ABC & );
    //...
};
class D : public ABC {
    //...
};

The constructors are declared protected to allow their use by derived class constructors, while preventing the creation of standalone ABC objects.

void func1( ABC );
void func2( ABC & );
ABC a; // error! protected default ctor                  
D d; // OK
func1( d ); // error! protected copy ctor                
func2( d ); // OK, no copy ctor

Another way to coerce a class into being an abstract base class is to bite the bullet and designate one of its virtual functions as pure. Often the destructor is a good choice:

class ABC {
  public:
    virtual ~ABC() = 0;
    //...
};
//...
ABC::~ABC() { ... }

Note that it is necessary, in this case, to provide an implementation of the pure virtual function since derived class destructors will call their base class destructors implicitly. (Note that this implicit call to a base class destructor from within a derived class destructor is always a non-vitual call.)

A third approach applies when a class has no virtual functions at all and no need for explicitly declared constructors. In this case, a protected, non-virtual destructor is a good approach.

class ABC {
  protected:
    ~ABC();
  public:
    //...
};

A protected destructor has basically the same effect as a protected constructor, but the error occurs when the object goes out of scope or is explicitly destroyed rather than when it is created:

void someFunc() {
    ABC a; // no error yet...
    ABC *p = new ABC; // no error yet...
    //...
    delete p; // error! protected dtor                   
    // error! implicit call to a's dtor                  
}