Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
When operator new can't satisfy a memory allocation request, it throws an exception. Long ago, it returned a null pointer, and some older compilers still do that. You can still get the old behavior (sort of), but I'll defer that discussion until the end of this Item. Before operator new throws an exception in response to an unsatisfiable request for memory, it calls a client-specifiable error-handling function called a new-handler. (This is not quite true. What operator new really does is a bit more complicated. Details are provided in Item 51.) To specify the out-of-memory-handling function, clients call set_new_handler, a standard library function declared in <new>: namespace std { typedef void (*new_handler)(); new_handler set_new_handler(new_handler p) throw(); }
As you can see, new_handler is a typedef for a pointer to a function that takes and returns nothing, and set_new_handler is a function that takes and returns a new_handler. (The "throw()" at the end of set_new_handler's declaration is an exception specification. It essentially says that this function won't throw any exceptions, though the truth is a bit more interesting. For details, see Item 29.) set_new_handler's parameter is a pointer to the function operator new should call if it can't allocate the requested memory. The return value of set_new_handler is a pointer to the function in effect for that purpose before set_new_handler was called. You use set_new_handler like this: // function to call if operator new can't allocate enough memory void outOfMem() { std::cerr << "Unable to satisfy request for memory\n"; std::abort(); } int main() { std::set_new_handler(outOfMem); int *pBigDataArray = new int[100000000L]; ... } If operator new is unable to allocate space for 100,000,000 integers, outOfMem will be called, and the program will abort after issuing an error message. (By the way, consider what happens if memory must be dynamically allocated during the course of writing the error message to cerr....) When operator new is unable to fulfill a memory request, it calls the new-handler function repeatedly until it can find enough memory. The code giving rise to these repeated calls is shown in Item 51, but this high-level description is enough to conclude that a well-designed new-handler function must do one of the following:
These choices give you considerable flexibility in implementing new-handler functions. Sometimes you'd like to handle memory allocation failures in different ways, depending on the class of the object being allocated: class X { public: static void outOfMemory(); ... }; class Y { public: static void outOfMemory(); ... }; X* p1 = new X; // if allocation is unsuccessful, // call X::outOfMemory Y* p2 = new Y; // if allocation is unsuccessful, // call Y::outOfMemory
C++ has no support for class-specific new-handlers, but it doesn't need any. You can implement this behavior yourself. You just have each class provide its own versions of set_new_handler and operator new. The class's set_new_handler allows clients to specify the new-handler for the class (exactly like the standard set_new_handler allows clients to specify the global new-handler). The class's operator new ensures that the class-specific new-handler is used in place of the global new-handler when memory for class objects is allocated. Suppose you want to handle memory allocation failures for the Widget class. You'll have to keep track of the function to call when operator new can't allocate enough memory for a Widget object, so you'll declare a static member of type new_handler to point to the new-handler function for the class. Widget will look something like this: class Widget { public: static std::new_handler set_new_handler(std::new_handler p) throw(); static void * operator new(std::size_t size) throw(std::bad_alloc); private: static std::new_handler currentHandler; };
Static class members must be defined outside the class definition (unless they're const and integral see Item 2), so: std::new_handler Widget::currentHandler = 0; // init to null in the class // impl. file
The set_new_handler function in Widget will save whatever pointer is passed to it, and it will return whatever pointer had been saved prior to the call. This is what the standard version of set_new_handler does: std::new_handler Widget::set_new_handler(std::new_handler p) throw() { std::new_handler oldHandler = currentHandler; currentHandler = p; return oldHandler; }
Finally, Widget's operator new will do the following:
Here's how you say all that in C++. We'll begin with the resource-handling class, which consists of nothing more than the fundamental RAII operations of acquiring a resource during construction and releasing it during destruction (see Item 13): class NewHandlerHolder { public: explicit NewHandlerHolder(std::new_handler nh) // acquire current :handler(nh) {} // new-handler ~NewHandlerHolder() // release it { std::set_new_handler(handler); } private: std::new_handler handler; // remember it NewHandlerHolder(const NewHandlerHolder&); // prevent copying NewHandlerHolder& // (see Item 14) operator=(const NewHandlerHolder&); };
This makes implementation of Widget's operator new quite simple: void * Widget::operator new(std::size_t size) throw(std::bad_alloc) { NewHandlerHolder // install Widget's h(std::set_new_handler(currentHandler)); // new-handler return ::operator new(size); // allocate memory // or throw } // restore global // new-handler
Clients of Widget use its new-handling capabilities like this: void outOfMem(); // decl. of func. to call if mem. alloc. // for Widget objects fails Widget::set_new_handler(outOfMem); // set outOfMem as Widget's // new-handling function Widget *pw1 = new Widget; // if memory allocation // fails, call outOfMem std::string *ps = new std::string; // if memory allocation fails, // call the global new-handling // function (if there is one) Widget::set_new_handler(0); // set the Widget-specific // new-handling function to // nothing (i.e., null) Widget *pw2 = new Widget; // if mem. alloc. fails, throw an // exception immediately. (There is // no new- handling function for // class Widget.) The code for implementing this scheme is the same regardless of the class, so a reasonable goal would be to reuse it in other places. An easy way to make that possible is to create a "mixin-style" base class, i.e., a base class that's designed to allow derived classes to inherit a single specific capability in this case, the ability to set a class-specific new-handler. Then turn the base class into a template, so that you get a different copy of the class data for each inheriting class. The base class part of this design lets derived classes inherit the set_new_handler and operator new functions they all need, while the template part of the design ensures that each inheriting class gets a different currentHandler data member. That may sound a bit complicated, but the code looks reassuringly familiar. In fact, the only real difference is that it's now available to any class that wants it: template<typename T> // "mixin-style" base class for class NewHandlerSupport{ // class-specific set_new_handler public: // support static std::new_handler set_new_handler(std::new_handler p) throw(); static void * operator new(std::size_t size) throw(std::bad_alloc); ... // other versions of op. new // see Item 52 private: static std::new_handler currentHandler; }; template<typename T> std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw() { std::new_handler oldHandler = currentHandler; currentHandler = p; return oldHandler; } template<typename T> void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc) { NewHandlerHolder h(std::set_new_handler(currentHandler)); return ::operator new(size); } // this initializes each currentHandler to null template<typename T> std::new_handler NewHandlerSupport<T>::currentHandler = 0; With this class template, adding set_new_handler support to Widget is easy: Widget just inherits from NewHandlerSupport<Widget>. (That may look peculiar, but I'll explain in more detail below exactly what's going on.) class Widget: public NewHandlerSupport<Widget> { ... // as before, but without declarations for }; // set_new_handler or operator new That's all Widget needs to do to offer a class-specific set_new_handler. But maybe you're still fretting over Widget inheriting from NewHandlerSupport<Widget>. If so, your fretting may intensify when you note that the NewHandlerSupport template never uses its type parameter T. It doesn't need to. All we need is a different copy of NewHandlerSupport in particular, its static data member currentHandler for each class that inherits from NewHandlerSupport. The template parameter T just distinguishes one inheriting class from another. The template mechanism itself automatically generates a copy of currentHandler for each T with which NewHandlerSupport is instantiated. As for Widget inheriting from a templatized base class that takes Widget as a type parameter, don't feel bad if the notion makes you a little woozy. It initially has that effect on everybody. However, it turns out to be such a useful technique, it has a name, albeit one that reflects the fact that it looks natural to no one the first time they see it. It's called the curiously recurring template pattern (CRTP). Honest. At one point, I published an article suggesting that a better name would be "Do It For Me," because when Widget inherits from NewHandlerSupport<Widget>, it's really saying, "I'm Widget, and I want to inherit from the NewHandlerSupport class for Widget." Nobody uses my proposed name (not even me), but thinking about CRTP as a way of saying "do it for me" may help you understand what the templatized inheritance is doing. Templates like NewHandlerSupport make it easy to add a class-specific new-handler to any class that wants one. Mixin-style inheritance, however, invariably leads to the topic of multiple inheritance, and before starting down that path, you'll want to read Item 40. Until 1993, C++ required that operator new return null when it was unable to allocate the requested memory. operator new is now specified to throw a bad_alloc exception, but a lot of C++ was written before compilers began supporting the revised specification. The C++ standardization committee didn't want to abandon the test-for-null code base, so they provided alternative forms of operator new that offer the traditional failure-yields-null behavior. These forms are called "nothrow" forms, in part because they employ nothrow objects (defined in the header <new>) at the point where new is used: class Widget { ... }; Widget *pw1 = new Widget; // throws bad_alloc if // allocation fails if (pw1 == 0) ... // this test must fail Widget *pw2 =new (std::nothrow) Widget; // returns 0 if allocation for // the Widget fails if (pw2 == 0) ... // this test may succeed
Nothrow new offers a less compelling guarantee about exceptions than is initially apparent. In the expression "new (std::nothrow) Widget," two things happen. First, the nothrow version of operator new is called to allocate enough memory for a Widget object. If that allocation fails, operator new returns the null pointer, just as advertised. If it succeeds, however, the Widget constructor is called, and at that point, all bets are off. The Widget constructor can do whatever it likes. It might itself new up some memory, and if it does, it's not constrained to use nothrow new. Although the operator new call in "new (std::nothrow) Widget" won't throw, then, the Widget constructor might. If it does, the exception will be propagated as usual. Conclusion? Using nothrow new guarantees only that operator new won't throw, not that an expression like "new (std::nothrow) Widget" will never yield an exception. In all likelihood, you will never have a need for nothrow new. Regardless of whether you use "normal" (i.e., exception-throwing) new or its somewhat stunted nothrow cousin, it's important that you understand the behavior of the new-handler, because it's used with both forms. Things to Remember
|