Reading Notes Efficient c++ Item 52 If you implement placement new, you also implement placement delete

Keywords: C++

1. What happens when an exception is thrown by calling the normal version of operator new?

Placement new and placement delete are not the most common beasts in C++ zoos, so you don't have to worry about being unfamiliar with them. When you implement a new expression like this, recall Item 16 and Item 17:

1 Widget *pw = new Widget;

 

Two functions are called: one is to call operator new to allocate memory, and the second is the default constructor of the Widget.

Suppose the first call succeeds, but the call to the second function throws an exception. In this case, the memory allocation performed in Step 1 must be rolled back. Otherwise, memory leaks will occur. Client code cannot release memory because if the Widget constructor throws an exception, pw will never be assigned. There is no way for the client to get a pointer to the memory that needs to be freed. The responsibility for rolling back step one falls on the C++ runtime system.

The runtime system is happy to call operator delete corresponding to the operator new version called in step 1, but it can only do so if it knows which operator delete (there may be many) is the appropriate called function. If the new and delete versions you are dealing with have normal signatures, then this is not a problem, because the normal operator new,

1 void* operator new(std::size_t) throw(std::bad_alloc);

 

Corresponding to the normal operator delete:

1 void operator delete(void *rawMemory) throw();     // normal signature
2 // at global scope
3 
4 void operator delete(void *rawMemory, std::size_t size) throw();   // typical normal signature at class  scope        

 

2. What happens when you call a custom operator new to throw an exception?

2.1 A problematic example

If you are using normal forms of new and delete, the runtime system can find the corresponding version of new delete to perform rollback operations. However, if you start declaring a non-ordinary version of new -- that is, to generate a parameterized version, "which delete is the corresponding version of new" comes up.

 

For example, suppose you implement a class-specific version of operator new, which requires specifying an ostream to record memory allocation information. You also implement a common class-specific version of operator delete:

 1 class Widget {
 2 public:
 3 ...
 4 
 5 static void* operator new(std::size_t size,
 6 
 7                                                          // non-normal
 8 std::ostream& logStream)                    // form of new
 9 throw(std::bad_alloc);
10 static void operator delete(void *pMemory, // normal class
11 std::size_t size) throw(); // specific form
12 // of delete
13 ...
14 };

 

2.2 Explanation of related terms

This design is problematic, but before we discuss the reasons, we need to explain the relevant terms.

When an operator new function takes additional parameters (except the size_t parameter that must be taken), we know that this is the placement version of new. The operator new above is such a placement version. A particularly useful placement new is with a pointer parameter specifying where the object should be built. It will look like the following:

1 void* operator new(std::size_t, void *pMemory) throw(); // "placement
2 // new"

 

This version of new is part of the C++ standard library and can be accessed as long as you # inlucde < New >. It is also used to create objects in unused spaces of vector s. It was also the earliest placement new. In fact, this is also the basis for naming this function: new at a specific location. This means that "placement new" is overloaded. Most of the time when people talk about placement new, they talk about this particular function, operator new with a void * extra parameter. In a few cases, they discuss any version of operator new with additional parameters. The context of a program tends to clear up this ambiguity, but it's important to understand that the common term "placement new" means any new version with additional parameters, because "placement delete" (which we'll encounter later) derives directly from it.

2.3 How to Solve the Problem

Now let's go back to the declaration of the Widget class, which I said earlier was problematic. The difficulty is that subtle memory leaks can occur in this class. Consider the following client code, which records memory allocation information in cerr when creating a widget dynamically:

1 Widget *pw = new (std::cerr) Widget; // call operator new, passing cerr as
2 // the ostream; this leaks memory
3 // if the Widget constructor throws

 

Last time, when the memory allocation was successful, but the Widget constructor throws an exception, the runtime system has the responsibility to roll back the allocation performed by operator new. However, the runtime system cannot really understand how the called operator new version works, so it cannot roll back itself. Instead, the runtime system looks for an operator delete with the same number and type of additional parameters as operator new, and if found, this is the version it calls. In the example above, operator new takes an additional parameter ostream &, so the corresponding operator delete is:

1 void operator delete(void*, std::ostream&) throw();

 

In contrast to the new placement version, the operator delete version with additional parameters is called placement delete. In this case, the Widget does not declare the placement version of operator delete, so the runtime system does not know how to roll back the placement new operation. So it won't do anything. In this example, if the Widget constructor throws an exception, no operator delete will be called!

The rule is simple: if an operator delete version with additional parameters operator new does not match it with the same additional parameters, no operator delete will be invoked if the memory allocation operation of new needs to be rolled back. To eliminate the memory leak of the above code, Widget needs to declare a placement delete corresponding to the placement new version of the log:

 1 class Widget {
 2 public:
 3 ...            
 4 
 5 static void* operator new(std::size_t size, std::ostream& logStream)
 6 throw(std::bad_alloc);
 7 static void operator delete(void *pMemory) throw();
 8 static void operator delete(void *pMemory, std::ostream& logStream)
 9 throw();
10 ...
11 };

 

With this change, in the following statement, if an exception is thrown from the Widget constructor:

1 Widget *pw = new (std::cerr) Widget; // as before, but no leak this time

 

The corresponding placement delete is automatically invoked, which allows the Widget to ensure that no memory is leaked.

 

3. What happens when delete is called?

However, considering what happens if no exception is thrown, we delete in the client code:

1 delete pw; // invokes the normal
2 // operator delete

 

As explained in the comment, this calls the normal operator delete instead of the placement version. Placement delete is triggered only when an exception is thrown when the matching placement new is called in the constructor. Using delete for a pointer (just like the pw above) never calls the location version of delete.

This means that in order to pre-empt the memory leak caused by the new placement version, you must also provide a common version of operator delete (called when no exception is thrown during construction) and a placement version with the same additional parameters as placement new (called when an exception is thrown). To do this, you never have to toss and turn to sleep on the subtle issue of memory leaks.

4. Pay attention to name hiding

By the way, because the name of the member function hides the same name in the peripheral scope (see Item 33 You need to be careful not to hide other versions (including regular versions) that customers need from class-specific new versions. For example, if you have a base class that only declares a place version of operator new, customers will find that they can no longer use the normal version of new:

 1 class Base {
 2 public:
 3 ...
 4 static void* operator new(std::size_t size, // this new hides
 5 std::ostream& logStream) // the normal
 6 throw(std::bad_alloc); // global forms
 7 ...
 8 };
 9 
10 Base *pb = new Base;                  // error! the normal form of
11 // operator new is hidden
12 
13 Base *pb = new (std::cerr) Base; // fine, calls Base's
14 // placement new

 

Similarly, operator new in a derived class hides both the global and inherited versions of operator new:

 1 class Derived: public Base {         // inherits from Base above
 2 
 3 
 4 public:
 5 ...
 6 static void* operator new(std::size_t size) // redeclares the normal
 7 throw(std::bad_alloc); // form of new
 8 ...
 9 };
10 Derived *pd = new (std::clog) Derived; // error! Base's placement
11 // new is hidden
12 Derived *pd = new Derived; // fine, calls Derived's
13 // operator new

 

Item 33 This type of name hiding is discussed in great detail, but in order to implement memory allocation functions, you need to remember that by default, C++ provides the following version of operator new globally:

1 void* operator new(std::size_t) throw(std::bad_alloc);          // normal new
2 
3 void* operator new(std::size_t, void*) throw();    // placement new
4 
5 void* operator new(std::size_t,                             // nothrow new —
6 const std::nothrow_t&) throw(); // see Item 49

 

If you declare any operator new in the class, you will hide the standard versions. Unless your intention is to prevent customers from using these versions, make sure that these standard versions are available to customers, in addition to any custom operator new versions you create. For each operator new you provide, make sure that the corresponding operator delete is provided at the same time. If you want these functions to behave like normal functions, let your class-specific version call the global version.

A simple way to do this is to create a base class that contains all new and delete versions:

 1 class StandardNewDeleteForms {
 2 public:
 3 // normal new/delete
 4 static void* operator new(std::size_t size) throw(std::bad_alloc)
 5 { return ::operator new(size); }
 6 static void operator delete(void *pMemory) throw()
 7 { ::operator delete(pMemory); }
 8 
 9 // placement new/delete
10 static void* operator new(std::size_t size, void *ptr) throw()
11 { return ::operator new(size, ptr); }
12 static void operator delete(void *pMemory, void *ptr) throw()
13 { return ::operator delete(pMemory, ptr); }
14 // nothrow new/delete
15 static void* operator new(std::size_t size, const std::nothrow_t& nt) throw()
16 { return ::operator new(size, nt); }
17 static void operator delete(void *pMemory, const std::nothrow_t&) throw()
18 { ::operator delete(pMemory); }
19 };

 

If a customer wants to add a standard version based on a customized version, he just needs to inherit the base class and use the using declaration. Item 33 (Obtain the standard version:

 1 class Widget: public StandardNewDeleteForms {       // inherit std forms
 2 
 3 public:                                                               
 4 
 5 
 6 using StandardNewDeleteForms::operator new; // make those
 7 
 8 using StandardNewDeleteForms::operator delete;    // forms visible
 9 
10 static void* operator new(std::size_t size,        // add a custom
11 
12 
13 std::ostream& logStream) // placement new
14 throw(std::bad_alloc);
15 static void operator delete(void *pMemory, // add the corres
16 std::ostream& logStream) // ponding place
17 throw(); // ment delete
18 ...
19 };

 

5. summary

 

  • When you implement the placement version of operator new, make sure you implement the corresponding operator delete placement version. If you don't implement it, some programs will have subtle, intermittent memory leaks.
  • When you declare placement versions of new and delete, make sure you don't inadvertently hide the normal versions of these functions.

Posted by pete07920 on Wed, 19 Dec 2018 19:51:05 -0800