More Books
C++ Gotchas: Avoiding Common Problems in Coding and Design
Main Page
Table of content
Copyright
Addison-Wesley Professional Computing Series
Preface
Acknowledgments
Chapter 1. Basics
Gotcha #1: Excessive Commenting
Gotcha #2: Magic Numbers
Gotcha #3: Global Variables
Gotcha #4: Failure to Distinguish Overloading from Default Initialization
Gotcha #5: Misunderstanding References
Gotcha #6: Misunderstanding Const
Gotcha #7: Ignorance of Base Language Subtleties
Gotcha #8: Failure to Distinguish Access and Visibility
Gotcha #9: Using Bad Language
Gotcha #10: Ignorance of Idiom
Gotcha #11: Unnecessary Cleverness
Gotcha #12: Adolescent Behavior
Chapter 2. Syntax
Gotcha #13: Array/Initializer Confusion
Gotcha #14: Evaluation Order Indecision
Gotcha #15: Precedence Problems
Gotcha #16: 'for' Statement Debacle
Gotcha #17: Maximal Munch Problems
Gotcha #18: Creative Declaration-Specifier Ordering
Gotcha #19: Function/Object Ambiguity
Gotcha #20: Migrating Type-Qualifiers
Gotcha #21: Self-Initialization
Gotcha #22: Static and Extern Types
Gotcha #23: Operator Function Lookup Anomaly
Gotcha #24: Operator '->' Subtleties
Chapter 3. The Preprocessor
Gotcha #25: '#define' Literals
Gotcha #26: '#define' Pseudofunctions
Gotcha #27: Overuse of '#if'
Gotcha #28: Side Effects in Assertions
Chapter 4. Conversions
Gotcha #29: Converting through 'void *'
Gotcha #30: Slicing
Gotcha #31: Misunderstanding Pointer-to-Const Conversion
Gotcha #32: Misunderstanding Pointer-to-Pointer-to-Const Conversion
Gotcha #33: Misunderstanding Pointer-to-Pointer-to-Base Conversion
Gotcha #34: Pointer-to-Multidimensional-Array Problems
Gotcha #35: Unchecked Downcasting
Gotcha #36: Misusing Conversion Operators
Gotcha #37: Unintended Constructor Conversion
Gotcha #38: Casting under Multiple Inheritance
Gotcha #39: Casting Incomplete Types
Gotcha #40: Old-Style Casts
Gotcha #41: Static Casts
Gotcha #42: Temporary Initialization of Formal Arguments
Gotcha #43: Temporary Lifetime
Gotcha #44: References and Temporaries
Gotcha #45: Ambiguity Failure of 'dynamic_cast'
Gotcha #46: Misunderstanding Contravariance
Chapter 5. Initialization
Gotcha #47: Assignment/Initialization Confusion
Gotcha #48: Improperly Scoped Variables
Gotcha #49: Failure to Appreciate C++'s Fixation on Copy Operations
Gotcha #50: Bitwise Copy of Class Objects
Gotcha #51: Confusing Initialization and Assignment in Constructors
Gotcha #52: Inconsistent Ordering of the Member Initialization List
Gotcha #53: Virtual Base Default Initialization
Gotcha #54: Copy Constructor Base Initialization
Gotcha #55: Runtime Static Initialization Order
Gotcha #56: Direct versus Copy Initialization
Gotcha #57: Direct Argument Initialization
Gotcha #58: Ignorance of the Return Value Optimizations
Gotcha #59: Initializing a Static Member in a Constructor
Chapter 6. Memory and Resource Management
Gotcha #60: Failure to Distinguish Scalar and Array Allocation
Gotcha #61: Checking for Allocation Failure
Gotcha #62: Replacing Global New and Delete
Gotcha #63: Confusing Scope and Activation of Member 'new' and 'delete'
Gotcha #64: Throwing String Literals
Gotcha #65: Improper Exception Mechanics
Gotcha #66: Abusing Local Addresses
Gotcha #67: Failure to Employ Resource Acquisition Is Initialization
Gotcha #68: Improper Use of 'auto_ptr'
Chapter 7. Polymorphism
Gotcha #69: Type Codes
Gotcha #70: Nonvirtual Base Class Destructor
Gotcha #71: Hiding Nonvirtual Functions
Gotcha #72: Making Template Methods Too Flexible
Gotcha #73: Overloading Virtual Functions
Gotcha #74: Virtual Functions with Default Argument Initializers
Gotcha #75: Calling Virtual Functions in Constructors and Destructors
Gotcha #76: Virtual Assignment
Gotcha #77: Failure to Distinguish among Overloading, Overriding, and Hiding
Gotcha #78: Failure to Grok Virtual Functions and Overriding
Gotcha #79: Dominance Issues
Chapter 8. Class Design
Gotcha #80: Get/Set Interfaces
Gotcha #81: Const and Reference Data Members
Gotcha #82: Not Understanding the Meaning of Const Member Functions
Gotcha #83: Failure to Distinguish Aggregation and Acquaintance
Gotcha #84: Improper Operator Overloading
Gotcha #85: Precedence and Overloading
Gotcha #86: Friend versus Member Operators
Gotcha #87: Problems with Increment and Decrement
Gotcha #88: Misunderstanding Templated Copy Operations
Chapter 9. Hierarchy Design
Gotcha #89: Arrays of Class Objects
Gotcha #90: Improper Container Substitutability
Gotcha #91: Failure to Understand Protected Access
Gotcha #92: Public Inheritance for Code Reuse
Gotcha #93: Concrete Public Base Classes
Gotcha #94: Failure to Employ Degenerate Hierarchies
Gotcha #95: Overuse of Inheritance
Gotcha #96: Type-Based Control Structures
Gotcha #97: Cosmic Hierarchies
Gotcha #98: Asking Personal Questions of an Object
Gotcha #99: Capability Queries
Bibliography

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.