Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
Item 52: Write placement delete if you write placement new
Placement new and placement delete aren't the most commonly encountered beasts in the C++ menagerie, so don't worry if you're not familiar with them. Instead, recall from Items 16 and 17 that 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, // typical normal std::size_t size) throw(); // signature at class // scope When you're using only the normal forms of new and delete, then, the runtime system has no trouble finding the delete that knows how to undo what new did. The which-delete-goes-with-this-new issue does arise, however, when you start declaring non-normal forms of operator new forms that take additional parameters. For example, suppose you write a class-specific operator new that requires specification of an ostream to which allocation information should be logged, and you also write a normal class-specific operator delete: class Widget { public: ... static void* operator new(std::size_t size, // non-normal std::ostream& logStream) // form of new throw(std::bad_alloc); static void operator delete(void *pMemory // normal class- std::size_t size) throw(); // specific form // of delete ... }; This design is problematic, but before we see why, we need to make a brief terminological detour. 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" This version of new is part of C++'s standard library, and you have access to it whenever you #include <new>. Among other things, this new is used inside vector to create objects in the vector's unused capacity. It's also the original placement new. In fact, that's how this function is known: as placement new. Which means that the term "placement new" is overloaded. Most of the time when people talk about placement new, they're talking about this specific function, the operator new taking a single extra argument of type void*. Less commonly, they're talking about any version of operator new that takes extra arguments. Context generally clears up any ambiguity, but it's important to understand that the general term "placement new" means any version of new taking extra arguments, because the phrase "placement delete" (which we'll encounter in a moment) derives directly from it. But let's get back to the declaration of the Widget class, the one whose design I said was problematic. The difficulty is that this class will give rise to subtle memory leaks. 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. Well, at least not these subtle memory leaks. 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, // this new hides std::ostream& logStream) // the normal throw(std::bad_alloc); // 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: class Derived: public Base { // inherits from Base above public: ... static void* operator new(std::size_t size) // redeclares the normal throw(std::bad_alloc); // 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, // nothrow new const std::nothrow_t&) throw(); // 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: class Widget: public StandardNewDeleteForms { // inherit std forms public: using StandardNewDeleteForms::operator new; // make those using StandardNewDeleteForms::operator delete; // forms visible static void* operator new(std::size_t size, // add a custom std::ostream& logStream) // placement new throw(std::bad_alloc); static void operator delete(void *pMemory, // add the corres- std::ostream& logStream) // ponding place- throw(); // ment delete ... };
Things to Remember
|