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> 你就可以访问它。需要指出,这个 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 版本时,确保不会无意中覆盖这些函数的常规版本。