- Failure to Distinguish Scalar and Array Allocation
- Checking for Allocation Failure
- Replacing Global New and Delete
- Confusing Scope and Activation of Member new and delete
- Throwing String Literals
- Improper Exception Mechanics
- Abusing Local Addresses
- Failure to Employ Resource Acquisition Is Initialization
- Improper Use of auto_ptr
Gotcha #62: Replacing Global New and Delete
It's almost never a good idea to replace the standard, global versions of operator new, operator delete, array new, or array delete, even though the standard permits it. The standard versions are typically highly optimized for general-purpose storage management, and user-defined replacements are unlikely to do better. (However, it's often reasonable to employ member memory-management operations to customize memory management for a specific class or hierarchy.)
Special-purpose versions of operator new and operator delete that implement different behavior from the standard versions will probably introduce bugs, since the correctness of much of the standard library and many third-party libraries depends on the default standard implementations of these functions.
A safer approach is to overload the global operator new rather than replace it. Suppose we'd like to fill newly allocated storage with a particular character pattern:
void *operator new( size_t n, const string &pat ) { char *p = static_cast<char *>(::operator new( n )); const char *pattern = pat.c_str(); if( !pattern || !pattern[0] ) pattern = "\0"; // note: two null chars const char *f = pattern; for( int i = 0; i < n; ++i ) { if( !*f ) f = pattern; p[i] = *f++; } return p; }
This version of operator new accepts a string pattern argument that is copied into the newly allocated storage. The compiler distinguishes between the standard operator new and our two-argument version through overload resolution.
string fill( "<garbage>" ); string *string1 = new string( "Hello" ); // standard version string *string2 = new (fill) string( "World!" ); // overloaded version
The standard also defines an overloaded operator new that takes, in addition to the required size_t first argument, a second argument of type void *. The implementation simply returns the second argument. (The throw() syntax is an exception-specification indicating that this function will not propagate any exceptions. It may be safely ignored in the following discussion, and in general.)
void *operator new( size_t, void *p ) throw() { return p; }
This is the standard "placement new," used to construct an object at a specific location. (Unlike with the standard, single-argument operator new, however, attempting to replace placement new is illegal.) Essentially, we use it to trick the compiler into calling a constructor for us. For example, for an embedded application, we may want to construct a "status register" object at a particular hardware address:
class StatusRegister { // . . . }; void *regAddr = reinterpret_cast<void *>(0XFE0000); // . . . // place register object at regAddr StatusRegister *sr = new (regAddr) StatusRegister;
Naturally, objects created with placement new must be destroyed at some point. However, since no memory is actually allocated by placement new, it's important to ensure that no memory is deleted. Recall that the behavior of the delete operator is to first activate the destructor of the object being deleted before calling an operator delete function to reclaim the storage. In the case of an object "allocated" with placement new, we must resort to an explicit destructor call to avoid any attempt to reclaim memory:
sr->~StatusRegister(); // explicit dtor call, no operator delete
Placement new and explicit destruction are clearly useful features, but they're just as clearly dangerous if not used sparingly and with caution. (See Gotcha #47 for one example from the standard library.)
Note that while we can overload operator delete, these overloaded versions will never be invoked by a standard delete-expression:
void *operator new( size_t n, Buffer &buffer ); // overloaded new void operator delete( void *p, Buffer &buffer ); // corresponding delete // . . . Thing *thing1 = new Thing; // use standard operator new Buffer buf; Thing *thing2 = new (buf) Thing; // use overloaded operator new delete thing2; // incorrect, should have used overloaded delete delete thing1; // correct, uses standard operator delete
Instead, as with an object created with placement new, we're forced to call the object's destructor explicitly, then explicitly deallocate the former object's storage with a direct call to the appropriate operator delete function:
thing2->~Thing(); // correct, destroy Thing operator delete( thing2, buf ); // correct, use overloaded delete
In practice, storage allocated by an overloaded global operator new is often erroneously deallocated by the standard global operator delete. One way to avoid this error is to ensure that any storage allocated by an overloaded global operator new obtains that storage from the standard global operator new. This is what we've done with the first overloaded implementation above, and our first version works correctly with standard global operator delete:
string fill( "<garbage>" ); string *string2 = new (fill) string( "World!" ); // . . . delete string2; // works
Overloaded versions of global operator new should, in general, either not allocate any storage or should be simple wrappers around the standard global operator new.
Often, the best approach is to avoid doing anything at all with global scope memory-management operator functions, but instead customize memory management on a class or hierarchy basis through the use of member operators new, delete, array new, and array delete.
We noted at the end of Gotcha #61 that an "appropriate" operator delete would be invoked by the runtime system in the event of an exception propagating out of an initialization in a new-expression:
Thing *tp = new Thing( arg );
If the allocation of Thing succeeds but the constructor for Thing throws an exception, the runtime system will invoke an appropriate operator delete to reclaim the uninitialized memory referred to by tp. In the case above, the appropriate operator delete would be either the global operator delete(void *) or a member operator delete with the same signature. However, a different operator new would imply a different operator delete:
Thing *tp = new (buf) Thing( arg );
In this case, the appropriate operator delete is the two-argument version corresponding to the overloaded operator new used for the allocation of Thing; operator delete( void *, Buffer &), and this is the version the runtime system will invoke.
C++ permits much flexibility in defining the behavior of memory management, but this flexibility comes at the cost of complexity. The standard, global versions of operator new and operator delete are sufficient for most needs. Employ more complex approaches only if they are clearly necessary.