[翻译] Effective C++, 3rd Edition, Item 52: 如果编写了 placement new,就要编写 placement delete

    技术2022-05-11  122

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

    作者:Scott Meyers

    译者:fatalerror99 (iTePub's Nirvana)

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

    在 C++ 动物园中,placement new 和 placement delete 并不是最常遇到的野兽,所以如果你和它们不熟也不必担心。作为替代,回想一下 Items 1617,当你写下一个这样的 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(识别特征)的 newdelete 版本,这不成问题,因为常规的 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

    当你只使用 newdelete 的常规形式时,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 持有额外的参数(除了那个必要的 size_t 参数),这个 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 abovepublic:  ...

      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

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

    达到这种效果的一个简单方法是创建一个包含 newdelete 的全部常规形式的 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 formspublic:   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(内存泄漏)。 当你声明 newdelete 的 placement 版本时,确保不会无意中覆盖这些函数的常规版本。

    最新回复(0)