Effective C++ 笔记 —— Item 49: Understand the behavior of the new-handler.

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 :

namespace std {
    typedef void(*new_handler)();
    new_handler set_new_handler(new_handler p) throw();
}

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]; 
    // ...
}

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:

  • Make more memory available. This may allow the next memory allocation attempt inside operator new to succeed. One way to implement this strategy is to allocate a large block of memory at program start-up, then release it for use in the program the first time the new-handler is invoked.
  • Install a different new-handler. If the current new-handler can’t make any more memory available, perhaps it knows of a different new-handler that can. If so, the current new-handler can install the other new-handler in its place (by calling set_new_handler). The next time operator new calls the new-handler function, it will get the one most recently installed. (A variation on this theme is for a new-handler to modify its own behavior, so the next time it’s invoked, it does something different. One way to achieve this is to have the new-handler modify static, namespace-specific, or global data that affects the new-handler’s behavior.)
  • Deinstall the new-handler, i.e., pass the null pointer to set_new_handler. With no new-handler installed, operator new will throw an exception when memory allocation is unsuccessful.
  • Throw an exception of type bad_alloc or some type derived from bad_alloc. Such exceptions will not be caught by operator new, so they will propagate to the site originating the request for memory.
  • Not return, typically by calling abort or exit.

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.

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;
};

std::new_handler Widget::currentHandler = 0; // init to null in the class impl. file

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:

  1. Call the standard set_new_handler with Widget's error-handling function. This installs Widget's new-handler as the global new-handler.
  2. Call the global operator new to perform the actual memory allocation. If allocation fails, the global operator new invokes Widget's new-handler, because that function was just installed as the global new-handler. If the global operator new is ultimately unable to allocate the memory, it throws a bad_alloc exception. In that case, Widget's operator new must restore the original global new-handler, then propagate the exception. To ensure that the original new-handler is always reinstated, Widget treats the global new-handler as a resource and follows the advice of Item 13 to use resource-managing objects to prevent resource leaks.
  3. If the global operator new was able to allocate enough memory for a Widget object, Widget's operator new returns a pointer to the allocated memory. The destructor for the object managing the global new-handler automatically restores the global new-handler to what it was prior to the call to Widget's operator new.

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 new-handler release it
        : handler(nh) 
    {
    }

    ~NewHandlerHolder()
    {
        std::set_new_handler(handler);
    }

private:
    std::new_handler handler; 
    
    // remember it prevent copying (see Item 14)
    NewHandlerHolder(const NewHandlerHolder&);
    NewHandlerHolder& 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)
{
    // install Widget's new-handler allocate memory or throw
    NewHandlerHolder h(std::set_new_handler(currentHandler)); 
    return ::operator new(size);
} // restore global new-handler

Clients of Widget use its new-handling capabilities like this:

// decl. of func. to call if mem. alloc. for Widget objects fails
void outOfMem(); 

// set outOfMem as Widget's new-handling function
Widget::set_new_handler(outOfMem); 

// if memory allocation fails, call outOfMem
Widget *pw1 = new Widget; 

// if memory allocation fails, call the global new-handling function (if there is one)
std::string *ps = new std::string; 

// set the Widget-specific new-handling function to nothing (i.e., null)
Widget::set_new_handler(0);

// if mem. alloc. fails, throw an exception immediately. (There is no new- handling function for no new-handling function for class Widget.)
Widget *pw2 = new 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.

// "mixin-style" base class for class-specific set_new_handler support
template<typename T> 
class NewHandlerSupport {
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);
    // ... 
    // 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. (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
};

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 mecha-nism itself automatically generates a copy of currentHandler for each T with which NewHandlerSupport is instantiated.

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 ) 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 returns 0 if allocation for

Widget *pw2 = new (std::nothrow) Widget;
if (pw2 == 0) ... // the Widget fails 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.

 

Things to Remember

  • set_new_handler allows you to specify a function to be called when memory allocation requests cannot be satisfied.
  • Nothrow new is of limited utility, because it applies only to memory allocation; associated constructor calls may still throw exceptions.

 

posted @ 2022-03-12 12:12  MyCPlusPlus  阅读(54)  评论(0编辑  收藏  举报