Item 7: Declare destructors virtual in polymorphic base classes


There are lots of ways to keep track of time, so it would be reasonable to create a TimeKeeper base class along with derived classes for different approaches to timekeeping:

 class TimeKeeper { public:   TimeKeeper();   ~TimeKeeper();   ... }; class AtomicClock: public TimeKeeper { ... }; class WaterClock: public TimeKeeper { ... }; class WristWatch: public TimeKeeper { ... }; 

Many clients will want access to the time without worrying about the details of how it's calculated, so a factory function a function that returns a base class pointer to a newly-created derived class object can be used to return a pointer to a timekeeping object:

 TimeKeeper* getTimeKeeper();       // returns a pointer to a dynamic-                                    // ally allocated object of a class                                    // derived from TimeKeeper 

In keeping with the conventions of factory functions, the objects returned by getTimeKeeper are on the heap, so to avoid leaking memory and other resources, it's important that each returned object be properly deleted:

 TimeKeeper *ptk = getTimeKeeper();  // get dynamically allocated object                                     // from TimeKeeper hierarchy ...                                 // use it delete ptk;                        // release it to avoid resource leak 

Item 13 explains that relying on clients to perform the deletion is error-prone, and Item 18 explains how the interface to the factory function can be modified to prevent common client errors, but such concerns are secondary here, because in this Item we address a more fundamental weakness of the code above: even if clients do everything right, there is no way to know how the program will behave.

The problem is that getTimeKeeper returns a pointer to a derived class object (e.g., AtomicClock), that object is being deleted via a base class pointer (i.e., a TimeKeeper* pointer), and the base class (TimeKeeper) has a non-virtual destructor. This is a recipe for disaster, because C++ specifies that when a derived class object is deleted through a pointer to a base class with a non-virtual destructor, results are undefined. What typically happens at runtime is that the derived part of the object is never destroyed. If getTimeKeeper were to return a pointer to an AtomicClock object, the AtomicClock part of the object (i.e., the data members declared in the AtomicClock class) would probably not be destroyed, nor would the AtomicClock destructor run. However, the base class part (i.e., the TimeKeeper part) typically would be destroyed, thus leading to a curious "partially destroyed" object. This is an excellent way to leak resources, corrupt data structures, and spend a lot of time with a debugger.

Eliminating the problem is simple: give the base class a virtual destructor. Then deleting a derived class object will do exactly what you want. It will destroy the entire object, including all its derived class parts:

 class TimeKeeper { public:   TimeKeeper();   virtual ~TimeKeeper();   ... }; TimeKeeper *ptk = getTimeKeeper(); ... delete ptk;                         // now behaves correctly 

Base classes like TimeKeeper generally contain virtual functions other than the destructor, because the purpose of virtual functions is to allow customization of derived class implementations (see Item 34). For example, TimeKeeper might have a virtual function, getCurrentTime, which would be implemented differently in the various derived classes. Any class with virtual functions should almost certainly have a virtual destructor.

If a class does not contain virtual functions, that often indicates it is not meant to be used as a base class. When a class is not intended to be a base class, making the destructor virtual is usually a bad idea. Consider a class for representing points in two-dimensional space:

 class Point {                           // a 2D point public:   Point(int xCoord, int yCoord);   ~Point(); private:   int x, y; }; 

If an int occupies 32 bits, a Point object can typically fit into a 64-bit register. Furthermore, such a Point object can be passed as a 64-bit quantity to functions written in other languages, such as C or FORTRAN. If Point's destructor is made virtual, however, the situation changes.

The implementation of virtual functions requires that objects carry information that can be used at runtime to determine which virtual functions should be invoked on the object. This information typically takes the form of a pointer called a vptr ("virtual table pointer"). The vptr points to an array of function pointers called a vtbl ("virtual table"); each class with virtual functions has an associated vtbl. When a virtual function is invoked on an object, the actual function called is determined by following the object's vptr to a vtbl and then looking up the appropriate function pointer in the vtbl.

The details of how virtual functions are implemented are unimportant. What is important is that if the Point class contains a virtual function, objects of that type will increase in size. On a 32-bit architecture, they'll go from 64 bits (for the two ints) to 96 bits (for the ints plus the vptr); on a 64-bit architecture, they may go from 64 to 128 bits, because pointers on such architectures are 64 bits in size. Addition of a vptr to Point will thus increase its size by 50 100%! No longer can Point objects fit in a 64-bit register. Furthermore, Point objects in C++ can no longer look like the same structure declared in another language such as C, because their foreign language counterparts will lack the vptr. As a result, it is no longer possible to pass Points to and from functions written in other languages unless you explicitly compensate for the vptr, which is itself an implementation detail and hence unportable.

The bottom line is that gratuitously declaring all destructors virtual is just as wrong as never declaring them virtual. In fact, many people summarize the situation this way: declare a virtual destructor in a class if and only if that class contains at least one virtual function.

It is possible to get bitten by the non-virtual destructor problem even in the complete absence of virtual functions. For example, the standard string type contains no virtual functions, but misguided programmers sometimes use it as a base class anyway:

 class SpecialString: public std::string {   // bad idea! std::string has a   ...                                       // non-virtual destructor }; 

At first glance, this may look innocuous, but if anywhere in an application you somehow convert a pointer-to-SpecialString into a pointer-to- string and you then use delete on the string pointer, you are instantly transported to the realm of undefined behavior:

 SpecialString *pss =   new SpecialString("Impending Doom"); std::string *ps; ... ps = pss;                               // SpecialString* std::string* ... delete ps;                              // undefined! In practice,                                         // *ps's SpecialString resources                                         // will be leaked, because the                                         // SpecialString destructor won't                                         // be called. 

The same analysis applies to any class lacking a virtual destructor, including all the STL container types (e.g., vector, list, set, tr1::unordered_map (see Item 54), etc.). If you're ever tempted to inherit from a standard container or any other class with a non-virtual destructor, resist the temptation! (Unfortunately, C++ offers no derivation-prevention mechanism akin to Java's final classes or C#'s sealed classes.)

Occasionally it can be convenient to give a class a pure virtual destructor. Recall that pure virtual functions result in abstract classes classes that can't be instantiated (i.e., you can't create objects of that type). Sometimes, however, you have a class that you'd like to be abstract, but you don't have any pure virtual functions. What to do? Well, because an abstract class is intended to be used as a base class, and because a base class should have a virtual destructor, and because a pure virtual function yields an abstract class, the solution is simple: declare a pure virtual destructor in the class you want to be abstract. Here's an example:

 class AWOV {                            // AWOV = "Abstract w/o Virtuals" public:   virtual ~AWOV() = 0;                  // declare pure virtual destructor }; 

This class has a pure virtual function, so it's abstract, and it has a virtual destructor, so you won't have to worry about the destructor problem. There is one twist, however: you must provide a definition for the pure virtual destructor:

 AWOV::~AWOV() {}                     // definition of pure virtual    dtor 

The way destructors work is that the most derived class's destructor is called first, then the destructor of each base class is called. Compilers will generate a call to ~AWOV from its derived classes' destructors, so you have to be sure to provide a body for the function. If you don't, the linker will complain.

The rule for giving base classes virtual destructors applies only to polymorphic base classes to base classes designed to allow the manipulation of derived class types through base class interfaces. TimeKeeper is a polymorphic base class, because we expect to be able to manipulate AtomicClock and WaterClock objects, even if we have only TimeKeeper pointers to them.

Not all base classes are designed to be used polymorphically. Neither the standard string type, for example, nor the STL container types are designed to be base classes at all, much less polymorphic ones. Some classes are designed to be used as base classes, yet are not designed to be used polymorphically. Such classes examples include Uncopyable from Item 6 and input_iterator_tag from the standard library (see Item 47) are not designed to allow the manipulation of derived class objects via base class interfaces. As a result, they don't need virtual destructors.

Things to Remember

  • Polymorphic base classes should declare virtual destructors. If a class has any virtual functions, it should have a virtual destructor.

  • Classes not designed to be base classes or not designed to be used polymorphically should not declare virtual destructors.




Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
ISBN: 321334876
EAN: N/A
Year: 2006
Pages: 102

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net