[翻譯] Effective C++, 3rd Edition, 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 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 就被稱爲 newplacement 版本。前面那個 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

如果你在一個 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 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(內存泄漏)。
  • 當你聲明 newdelete 的 placement 版本時,確保不會無意中覆蓋這些函數的常規版本。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章