[ Team LiB ] Previous Section Next Section

Gotcha #70: Nonvirtual Base Class Destructor

This subject has been covered in almost every C++ programming text over the past fifteen years. First, there is no better documentation that a class is, or is not, intended for use as a base class than the virtualness of its destructor. If the destructor isn't virtual, chances are it's not a base class.

Undefined Behavior

Publication of the standard has made this advice even more compelling. First, destroying a derived class through its base class interface now results in undefined behavior if the base class destructor is not virtual:



class Base { 


   Resource *br;


   // . . .


   ~Base() // note: nonvirtual


       { delete br; }


};


class Derived : public Base {


   OtherResource *dr;


   // . . .


   ~Derived()


       { delete dr; }


};


Base *bp = new Base;


// . . .


delete bp; // fine . . .


bp = new Derived;


// . . .


delete bp; // silent error!


Chances are you'll just get a call of the base class destructor for the derived class object: a bug. But the compiler may decide to do anything else it feels like (dump core? send nasty email to your boss? sign you up for a lifetime subscription to This Week in Object-Oriented COBOL?).

Virtual Static Member Functions

On the positive side, having a virtual destructor in a base class allows you to achieve the effect of a virtual static member function call. Virtual and static are mutually exclusive function-specifiers, and member memory-management operator functions (operators new, delete, new[], and delete[]) are static member functions. However, as with a virtual destructor, the most specialized member operator delete should be invoked during a deletion, particularly if there is a corresponding member operator new (see Gotcha #63):



class B { 


 public:


   virtual ~B();


   void *operator new( size_t );


   void operator delete( void *, size_t );


};


class D : public B {


 public:


   ~D();


   void *operator new( size_t );


   void operator delete( void *, size_t );


};


// . . .


B *bp = getABofSomeSort();


// . . .


delete bp; // call derived delete!


Thanks to the virtual destructor in the base class, the standard promises that we'll invoke the member operator delete in "the scope of the dynamic type of the class." That is, we'll probably invoke the member operator delete from within the derived class destructor. Since the derived class's destructor is (of course) in the scope of the derived class, the call will be to the derived class's operator delete.

In sum, even though operator delete is a static member function, the presence of a virtual destructor in the base class ensures that the derived-class-specific operator delete will be called even when performing the deletion through a base class pointer. In the code above, for instance, the deletion of the bp pointer will invoke D's destructor, followed by D's operator delete, and the second argument to the operator delete will be sizeof(D), not sizeof(B). Neat. Virtual statics.

Leading Them On

Older C++ code is often written with the assumption that, under single inheritance, the address of a base class subobject is the same as that of the complete object. (See Gotcha #29.)



class B { 


   int b1, b2;


};


class D : public B {


   int d1, d2;


};


D *dp = new D;


B *bp = dp;


While the standard makes no such promises, in this case the layout of a D object almost certainly starts with its B subobject, as in Figure 7-2.

Figure 7-2. Likely layout under single inheritance of an object that contains no virtual function. In this implementation, both the D complete object and its B subobject share the same initial address.

graphics/07fig02.gif

However, if the derived class declares a virtual function, the object will probably contain a virtual function table pointer (vptr) inserted implicitly by the compiler (see Gotcha #78). Two common object layouts are used in this case, shown in Figure 7-3.

Figure 7-3. Two possible layouts for an object under single inheritance, in which the derived class declares a virtual function and the base class does not. The layout on the left locates the virtual function table pointer at the end of the complete object, whereas the layout on the right locates it at the beginning, causing the base class subobject to be offset within the complete object.

graphics/07fig03.gif

In the first case, the tenuous assumption that the base subobject and derived object have the same address continues to hold, but it doesn't hold in the second case. Of course, the best way to deal with this problem is to rewrite any code that makes this nonstandard assumption. Typically, this means you have to stop using void * to hold class pointers (see Gotcha #29). Failing that, inserting a virtual function in the base class will make it more likely that an implementation will generate an object layout that will conform to the nonstandard assumption of address equivalence, as shown in Figure 7-4.

Figure 7-4. Likely layout of an object under single inheritance, in which the base class declares a virtual function

graphics/07fig04.gif

Usually, the best candidate for such a base class virtual function is a virtual destructor.

Exceptions

Even this most basic of idioms has exceptions. For instance, it's sometimes convenient to wrap a set of type names, static member functions, and static member data into a neat package:



namespace std { 


   template <class Arg, class Res>


   struct unary_function {


       typedef Arg argument_type;


       typedef Res result_type;


   };


}


In this case, a virtual destructor is unnecessary, because classes generated from this template have no resources to reclaim. The class has also been carefully designed to have no storage or execution time impact when used as a base class:



struct Expired : public unary_function<Deal *, bool> { 


   bool operator ()( const Deal *d ) const


       { return d->expired(); }


};


Finally, unary_function is part of the standard library. Experienced C++ programmers know not to treat it as a fully functional base class and will therefore not attempt to manipulate derived class objects through the unary_function interface. It's a special case.

Here's another example from a well-known but nonstandard library. The design constraints are the same in this case as for the standard base class above, but—because it's nonstandard—the author could not rely on the programmer's familiarity with the class:



namespace Loki { 


   struct OpNewCreator {


       template <class T>


       static T *Create() { return new T; }


     protected:


       ~OpNewCreator() {}


   };


}


The author's solution in this case was to declare a protected, inline, nonvirtual destructor. This retains the required space and time efficiency, makes it difficult to misuse the destructor, and is an explicit reminder that the class is not intended for use except as a base class.

These are exceptional cases, however, and it's generally good design practice to ensure that a base class has a virtual destructor.

    [ Team LiB ] Previous Section Next Section