Item 51: Adhere to convention when writing new and delete

Item 51: Adhere to convention when writing new and delete

Item 50 explains when you might want to write your own versions of operator new and operator delete, but it doesn't explain the conventions you must follow when you do it. The rules aren't hard to follow, but some of them are unintuitive, so it's important to know what they are.

We'll begin with operator new. Implementing a conformant operator new requires having the right return value, calling the new-handling function when insufficient memory is available (see Item 49), and being prepared to cope with requests for no memory. You'll also want to avoid inadvertently hiding the "normal" form of new, though that's more a class interface issue than an implementation requirement; it's addressed in Item 52.

The return value part of operator new is easy. If you can supply the requested memory, you return a pointer to it. If you can't, you follow the rule described in Item 49 and throw an exception of type bad_alloc.

It's not quite that simple, however, because operator new actually tries to allocate memory more than once, calling the new-handling function after each failure. The assumption here is that the new-handling function might be able to do something to free up some memory. Only when the pointer to the new-handling function is null does operator new throw an exception.

Curiously, C++ requires that operator new return a legitimate pointer even when zero bytes are requested. (Requiring this odd-sounding behavior simplifies things elsewhere in the language.) That being the case, pseudocode for a non-member operator new looks like this:

 void * operator new(std::size_t size) throw(std::bad_alloc) {                                      // your operator new might   using namespace std;                 // take additional params   if (size == 0) {                     // handle 0-byte requests     size = 1;                          // by treating them as   }                                    // 1-byte requests   while (true) {    attempt to allocate size bytes;     if (the allocation was successful)        return (a pointer to the memory);     // allocation was unsuccessful; find out what the     // current new-handling function is (see below)     new_handler globalHandler = set_new_handler(0);     set_new_handler(globalHandler);     if (globalHandler) (*globalHandler)();     else throw std::bad_alloc();   } } 

The trick of treating requests for zero bytes as if they were really requests for one byte looks slimy, but it's simple, it's legal, it works, and how often do you expect to be asked for zero bytes, anyway?

You may also look askance at the place in the pseudocode where the new-handling function pointer is set to null, then promptly reset to what it was originally. Unfortunately, there is no way to get at the new-handling function pointer directly, so you have to call set_new_handler to find out what it is. Crude, yes, but also effective, at least for single-threaded code. In a multithreaded environment, you'll probably need some kind of lock to safely manipulate the (global) data structures behind the new-handling function.

Item 49 remarks that operator new contains an infinite loop, and the code above shows that loop explicitly; "while (true)" is about as infinite as it gets. The only way out of the loop is for memory to be successfully allocated or for the new-handling function to do one of the things described in Item 49: make more memory available, install a different new-handler, deinstall the new-handler, throw an exception of or derived from bad_alloc, or fail to return. It should now be clear why the new-handler must do one of those things. If it doesn't, the loop inside operator new will never terminate.

Many people don't realize that operator new member functions are inherited by derived classes. That can lead to some interesting complications. In the pseudocode for operator new above, notice that the function tries to allocate size bytes (unless size is zero). That makes perfect sense, because that's the argument that was passed to the function. However, as Item 50 explains, one of the most common reasons for writing a custom memory manager is to optimize allocation for objects of a specific class, not for a class or any of its derived classes. That is, given an operator new for a class X, the behavior of that function is typically tuned for objects of size sizeof(X) nothing larger and nothing smaller. Because of inheritance, however, it is possible that the operator new in a base class will be called to allocate memory for an object of a derived class:

 class Base { public:   static void * operator new(std::size_t size) throw(std::bad_alloc);   ... }; class Derived: public Base                // Derived doesn't declare { ... };                                  // operator new Derived *p = new Derived;                 // calls Base::operator new! 

If Base's class-specific operator new wasn't designed to cope with this and chances are that it wasn't the best way for it to handle the situation is to slough off calls requesting the "wrong" amount of memory to the standard operator new, like this:

 void * Base::operator new(std::size_t size) throw(std::bad_alloc) {   if (size != sizeof(Base))               // if size is "wrong,"      return ::operator new(size);         // have standard operator                                           // new handle the request   ...                                     // otherwise handle                                           // the request here } 

"Hold on!" I hear you cry, "You forgot to check for the pathological-but-nevertheless-possible case where size is zero!" Actually, I didn't, and please stop using hyphens when you cry out. The test is still there, it's just been incorporated into the test of size against sizeof(Base). C++ works in some mysterious ways, and one of those ways is to decree that all freestanding objects have non-zero size (see Item 39). By definition, sizeof(Base) can never be zero, so if size is zero, the request will be forwarded to ::operator new, and it will become that function's responsibility to treat the request in a reasonable fashion.

If you'd like to control memory allocation for arrays on a per-class basis, you need to implement operator new's array-specific cousin, operator new[]. (This function is usually called "array new," because it's hard to figure out how to pronounce "operator new[]".) If you decide to write operator new[], remember that all you're doing is allocating a chunk of raw memory you can't do anything to the as-yet-nonexistent objects in the array. In fact, you can't even figure out how many objects will be in the array. First, you don't know how big each object is. After all, a base class's operator new[] might, through inheritance, be called to allocate memory for an array of derived class objects, and derived class objects are usually bigger than base class objects.

Hence, you can't assume inside Base::operator new[] that the size of each object going into the array is sizeof(Base), and that means you can't assume that the number of objects in the array is (bytes requested)/sizeof(Base). Second, the size_t parameter passed to operator new[] may be for more memory than will be filled with objects, because, as Item 16 explains, dynamically allocated arrays may include extra space to store the number of array elements.

So much for the conventions you need to follow when writing operator new. For operator delete, things are simpler. About all you need to remember is that C++ guarantees it's always safe to delete the null pointer, so you need to honor that guarantee. Here's pseudocode for a non-member operator delete:

 void operator delete(void *rawMemory) throw() {   if (rawMemory == 0) return;            // do nothing if the null                                          // pointer is being deleted   deallocate the memory pointed to by rawMemory; } 

The member version of this function is simple, too, except you've got to be sure to check the size of what's being deleted. Assuming your class-specific operator new forwards requests of the "wrong" size to ::operator new, you've got to forward "wrongly sized" deletion requests to ::operator delete:

 class Base {                            // same as before, but now public:                                 // operator delete is declared   static void * operator new(std::size_t size) throw(std::bad_alloc);   static void operator delete(void *rawMemory, std::size_t size) throw();   ... }; void Base::operator delete(void *rawMemory, std::size_t size) throw() {   if (rawMemory == 0) return;           // check for null pointer   if (size != sizeof(Base)) {           // if size is "wrong,"      ::operator delete(rawMemory);      // have standard operator      return;                            // delete handle the request   }   deallocate the memory pointed to by rawMemory;   return; } 

Interestingly, the size_t value C++ passes to operator delete may be incorrect if the object being deleted was derived from a base class lacking a virtual destructor. This is reason enough for making sure your base classes have virtual destructors, but Item 7 describes a second, arguably better reason. For now, simply note that if you omit virtual destructors in base classes, operator delete functions may not work correctly.

Things to Remember

  • operator new should contain an infinite loop trying to allocate memory, should call the new-handler if it can't satisfy a memory request, and should handle requests for zero bytes. Class-specific versions should handle requests for larger blocks than expected.

  • operator delete should do nothing if passed a pointer that is null. Class-specific versions should handle blocks that are larger than expected.

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
Year: 2006
Pages: 102 © 2008-2017.
If you may any questions please contact us: