When you write a new expression such as this:
Widget *pw = new Widget;
two functions are called: one to operator new to allocate memory, a second to Widget's default constructor. Suppose that the first call succeeds, but the second call results in an exception being thrown. In that case, the memory allocation performed in step 1 must be undone. Otherwise we'll have a memory leak. Client code can't deallocate the memory, because if the Widget constructor throws an exception, pw is never assigned. There'd be no way for clients to get at the pointer to the memory that should be deallocated. The responsibility for undoing step 1 must therefore fall on the C++ runtime system.
The runtime system is happy to call the operator delete that corresponds to the version of operator new it called in step 1, but it can do that only if it knows which operator delete — there may be many — is the proper one to call. This isn't an issue if you’re dealing with the versions of new and delete that have the normal signatures, because the normal operator new:
void* operator new(std::size_t) throw(std::bad_alloc);
corresponds to the normal operator delete:
void operator delete(void *rawMemory) throw(); // normal signature at global scope void operator delete(void *rawMemory, std::size_t size) throw(); // typical normal signature at class scope
When an operator new function takes extra parameters (other than the mandatory size_t argument), that function is known as a placement version of new. The operator new above is thus a placement version. A particularly useful placement new is the one that takes a pointer specifying where an object should be constructed. That operator new looks like this:
void* operator new(std::size_t, void *pMemory) throw(); // placement new
The general term "placement new" means any version of new taking extra arguments, because the phrase "placement delete" derives directly from it.
Consider this client code, which logs allocation information to cerr when dynamically creating a Widget:
Widget *pw = new (std::cerr) Widget; // call operator new, passing cerr as the ostream; this leaks memory if the Widget constructor throws
Once again, if memory allocation succeeds and the Widget constructor throws an exception, the runtime system is responsible for undoing the allocation that operator new performed. However, the runtime system can't really understand how the called version of operator new works, so it can't undo the allocation itself. Instead, the runtime system looks for a version of operator delete that takes the same number and types of extra arguments as operator new, and, if it finds it, that's the one it calls. In this case, operator new takes an extra argument of type ostream&, so the corresponding operator delete would have this signature:
void operator delete(void*, std::ostream&) throw();
By analogy with placement versions of new, versions of operator delete that take extra parameters are known as placement deletes. In this case, Widget declares no placement version of operator delete, so the runtime system doesn't know how to undo what the call to placement new does. As a result, it does nothing. In this example, no operator delete is called if the Widget constructor throws an exception!
The rule is simple: if an operator new with extra parameters isn't matched by an operator delete with the same extra parameters, no operator delete will be called if a memory allocation by the new needs to be undone. To eliminate the memory leak in the code above, Widget needs to declare a placement delete that corresponds to the logging placement new:
class Widget { public: // ... static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc); static void operator delete(void *pMemory) throw(); static void operator delete(void *pMemory, std::ostream& logStream) throw(); //... };
With this change, if an exception is thrown from the Widget constructor in this statement:
Widget *pw = new (std::cerr) Widget; // as before, but no leak this time
the corresponding placement delete is automatically invoked, and that allows Widget to ensure that no memory is leaked. However, consider what happens if no exception is thrown (which will usually be the case) and we get to a delete in client code:
delete pw; // invokes the normal operator delete
As the comment indicates, this calls the normal operator delete, not the placement version. Placement delete is called only if an exception arises from a constructor call that's coupled to a call to a placement new. Applying delete to a pointer (such as pw above) never yields a call to a placement version of delete. Never.
This means that to forestall all memory leaks associated with placement versions of new, you must provide both the normal operator delete (for when no exception is thrown during construction) and a placement version that takes the same extra arguments as operator new does (for when one is). Do that, and you'll never lose sleep over subtle memory leaks again.
Incidentally, because member function names hide functions with the same names in outer scopes (see Item 33), you need to be careful to avoid having class-specific news hide other news (including the normal versions) that your clients expect. For example, if you have a base class that declares only a placement version of operator new, clients will find that the normal form of new is unavailable to them:
class Base { public: // ... static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);// this new hides the normal global forms ... }; Base *pb = new Base; // error! the normal form of operator new is hidden Base *pb = new (std::cerr) Base; // fine, calls Base's placement new
Similarly, operator news in derived classes hide both global and inherited versions of operator new:
// inherits from Base above class Derived : public Base { public: // ... static void* operator new(std::size_t size) throw(std::bad_alloc); // redeclares the normal form of new // ... }; Derived *pd = new (std::clog) Derived; // error! Base's placement new is hidden Derived *pd = new Derived; // fine, calls Derived's operator new
Item 33 discusses this kind of name hiding in considerable detail, but for purposes of writing memory allocation functions, what you need to remember is that by default, C++ offers the following forms of operator new at global scope:
void* operator new(std::size_t) throw(std::bad_alloc); // normal new void* operator new(std::size_t, void*) throw(); // placement new void* operator new(std::size_t, const std::nothrow_t&) throw(); // nothrow new — see Item 49
If you declare any operator news in a class, you'll hide all these standard forms. Unless you mean to prevent class clients from using these forms, be sure to make them available in addition to any custom operator new forms you create. For each operator new you make available, of course, be sure to offer the corresponding operator delete, too. If you want these functions to behave in the usual way, just have your class-specific versions call the global versions. An easy way to do this is to create a base class containing all the normal forms of new and delete:
class StandardNewDeleteForms { public: // normal new/delete static void* operator new(std::size_t size) throw(std::bad_alloc) { return ::operator new(size); } static void operator delete(void *pMemory) throw() { ::operator delete(pMemory); } // placement new/delete static void* operator new(std::size_t size, void *ptr) throw() { return ::operator new(size, ptr); } static void operator delete(void *pMemory, void *ptr) throw() { return ::operator delete(pMemory, ptr); } // nothrow new/delete static void* operator new(std::size_t size, const std::nothrow_t& nt) throw() { return ::operator new(size, nt); } static void operator delete(void *pMemory, const std::nothrow_t&) throw() { ::operator delete(pMemory); } };
Clients who want to augment the standard forms with custom forms can then just use inheritance and using declarations (see Item 33) to get the standard forms:
// inherit std forms class Widget : public StandardNewDeleteForms { public: using StandardNewDeleteForms::operator new; // make those using StandardNewDeleteForms::operator delete; // forms visible static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc); // add a custom placement new static void operator delete(void *pMemory, std::ostream& logStream) throw(); //add the corresponding placement delete // ... };
Things to Remember
- When you write a placement version of operator new, be sure to write the corresponding placement version of operator delete. If you don't, your program may experience subtle, intermittent memory leaks.
- When you declare placement versions of new and delete, be sure not to unintentionally hide the normal versions of those functions.