Effective C++手册Item 52: 如果编写了 placement new,就要编写 placement delete

Item 52: 如果编写了 placement new,就要编写 placement delete

作者:Scott Meyers

译者:fatalerror99 (iTePub's Nirvana)

发布:http://blog.csdn.net/fatalerror99/

在 C++ 动物园中,placement new 和 placement delete 并不是最常遇到的野兽,所以如果你和它们不熟也不必担心。作为替代,回想一下 Items 16 和 17,当你写下一个这样的 new 表达式,

Widget *pw = new Widget;

有两个函数会被调用:一个是 operator new 用于分配内存,第二个是 Widget 的 default constructor(缺省构造函数)。

假设第一个调用成功,而第二个调用导致抛出一个 exception(异常)。这种情况下,第 1 步中完成的内存分配必须被撤销。否则就是一个内存泄漏。客户代码不可能回收这些内存,因为,如果 Widget 的 constructor(构造函数)抛出一个 exception(异常),pw 根本就没有被赋值。对于客户来说无法得到指向应该被回收的内存的指针。所以撤销第 1 步的职责必然落在了 C++ runtime system(C++ 运行时系统)的身上。

runtime system(运行时系统)恰当地调用与它在第 1 步中调用的 operator new 的版本相对应的 operator delete,但是只有在它知道哪一个 operator delete——可能有许多——最恰当的时候它才能做到这一点。如果你正在摆弄具有常规的 signatures(识别特征)的 new 和 delete 版本,这不成问题,因为常规的 operator new,

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

对应常规的 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

当你只使用 new 和 delete 的常规形式时,runtime system(运行时系统)找出知道如何撤销 new 所做的事情的 delete 没什么麻烦。然而,当你开始声明 operator new 的非常规形式——带有额外参数的形式的时候,which-delete-goes-with-this-new(哪一个 delete 和这个 new 配对)的问题就出现了。

例如,假设你编写了一个 class-specific(类专用)的 operator new,它需要一个用于记录分配信息的 ostream 的规格描述,而你又编写了一个常规的 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
  ...
};

这个设计是成问题的,但是在我们探究为什么之前,我们需要做一个简要的术语说明。

当一个 operator new function 持有额外的参数(除了那个必要的 sizet 参数),这个 function 就被称为 new 的 _placement 版本。前面那个 operator new 就是这样一个 placement 版本。有一个特别有用的 placement new,它持有一个指针,这个指针指定了一个 object 被构造的位置。那个 operator new 如下:

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

new 的这个版本是 C++ 标准库的一部分,只要 #include 你就可以访问它。需要指出,这个 new 用于 vector 内部,在 vector 的尚未使用的空间内创建 objects。它也是最初的 placement new。实际上,这就是这类函数被称为 placement new 的来历。这就意味着术语 "placement new" 被赋予了更多的含义。大多数情况下,当人们谈到 placement new,他们谈的就是这个特定的函数,持有一个 void* 类型的额外参数的 operator new。较少情况下,他们谈的是持有额外参数的 operator new 的任意版本。根据上下文通常可以搞清楚任何暧昧,重要的是要了解到通用术语 "placement new" 意味着持有额外参数的 new 的任意版本,因为短语 "placement delete"(过一会儿我们就会遇到它)直接起源于它。

我们让我们先返回到 Widget class 的 declaration(声明),就是我说设计成问题的那个。麻烦就在于这个 class 会引发微妙的 memory leaks(内存泄漏)。考虑如下客户代码,在动态创建一个 Widget 时,它将在 cerr 记录分配信息:

Widget *pw = new (std::cerr) Widget; // call operator new, passing cerr as
                                     // the ostream; _this leaks memory_
                                     // _if the Widget constructor throws_

重申一次,如果内存分配成功而 Widget constructor(构造函数)抛出一个 exception(异常),runtime system(运行时系统)有责任撤销 operator new 所执行的分配。然而,runtime system(运行时系统)不能真正了解被调用的 operator new 版本是如何工作的,所以它自己无法撤销那个分配。runtime system(运行时系统)转而寻找一个和 operator new 持有相同数量和类型额外参数的 operator delete 版本,而且,如果它找到了,它将调用它。在当前情况下,operator new 持有一个 ostream& 类型的额外参数,所以相应的 operator delete 应该具有这样的 signature(识别特征):

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

与 new 的 placement 版本类似,持有额外参数的 operator delete 版本被称为 placement deletes。当前情况下,Widget 没有声明 operator delete 的 placement 版本,所以 runtime system(运行时系统)不知道如何撤销所调用的 placement new 所做的事情。结果,它什么都不做。在本例中,如果 Widget constructor(构造函数)抛出一个 exception(异常),没有 operator delete 可以被调用!

规则很简单:如果一个带有额外参数的 operator new 没有带有同样额外参数的 operator delete 相匹配,当一个由 new 生成的内存分配需要撤销的时候没有 operator delete 可以被调用。为了消除前面的代码中的 memory leak(内存泄漏),Widget 需要声明一个与 logging placement new 相对应的 placement delete:

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();**
  ...
};

这样改变之后,如果从下面这个语句的 Widget constructor(构造函数)中抛出一个 exception(异常),

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

相应的 placement delete 自动被调用,而这就让 Widget 确保没有内存被泄漏。

然而,考虑以下情况会发生什么,如果没有抛出 exception(异常)(这是通常的情况)而我们的客户代码中又有一个 delete:

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

就像注释中所说的,这样将调用常规 operator delete,而不是 placement 版本。只有在调用一个与 placement new 相关联的 constructor(构造函数)时发生一个 exception(异常),placement delete 才会被调用。将 delete 施加于一个指针(诸如上面的 pw)绝对不会引起一个 delete 的 placement 版本的调用。绝对不会。

这就意味着为了预防所有与 new 的 placement 版本相关的 memory leaks(内存泄漏),你必须既提供常规 operator delete(用于构造过程中没有抛出 exception(异常)时),又要提供一个持有与 operator new 相同的 extra arguments(额外参数)的 placement 版本(用于相反情况)。这样,你就再也不会因为微妙的 memory leaks(内存泄漏)而睡不着觉了。好吧,至少是不会因为这里这些微妙的 memory leaks(内存泄漏)。

顺便说一下,因为 member function(成员函数)的名字会覆盖外围的具有相同名字的函数(参见 Item 33),你需要小心避免用 class-specific(类专用)的 news 覆盖你的客户所希望看到的其它 news(包括其常规版本)。例如,如果你有一个只声明了一个 operator new 的 placement 版本的 base class(基类),客户将发现 new 的常规形式对他们来说无法使用:

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

同样,derived classes(派生类)中的 operator news 覆盖 operator news 的全局和继承来的版本的 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 讨论了这种名字覆盖的需要考虑的细节,如果打算编写内存分配函数,你要记住,在缺省情况下,C++ 在全局范围提供如下形式的 operator new:

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](http://blog.csdn.net/fatalerror99/archive/2006/02/28/612673.aspx)

如果你在一个 class 中声明了任何 operator news,都将覆盖所有这些标准形式。除非你有意防止 class 的客户使用这些形式,否则,除了你创建的任何自定义 new 形式以外,还要确保它们都可以使用。当然,还要确保为每一个你使其可用的 operator new 提供相应的 operator delete。如果你要这些函数具有通常的行为,只需要让你的 class-specific(类专用)版本去调用 global(全局)版本即可。

达到这种效果的一个简单方法是创建一个包含 new 和 delete 的全部常规形式的 base class(基类):

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

想要在标准形式之外增加自定义形式的客户就能够使用 inheritance(继承)和 using declarations(使用声明)(参见 Item 33)来得到标准形式:

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

  • 在编写一个 operator new 的 placement 版本时,确保同时编写 operator delete 的相应的 placement 版本。否则,你的程序可能会发生微妙的,断续的 memory leaks(内存泄漏)。

  • 当你声明 new 和 delete 的 placement 版本时,确保不会无意中覆盖这些函数的常规版本。