天天看點

Effective C++之 Item 49: 了解 new-handler 的行為

當 operator new 不能滿足一個記憶體配置設定請求時,它抛出一個 exception(異常)。很久以前,他傳回一個 null pointer(空指針),而一些比較老的編譯器還在這樣做。你依然能得到以前的行為(在一定程度上),但是我要到這個 Item 的最後再讨論它。

在 operator new 因回應一個無法滿足的記憶體請求而抛出一個 exception 之前,它先調用一個可以由客戶指定的被稱為 new-handler 的 error-handling function(錯誤處理函數)。(這并不完全确切,operator new 真正做的事情比這個稍微複雜一些,詳細細節在 Item 51 提供。)為了指定 out-of-memory-handling function,客戶調用 set_new_handler ——一個在 <new> 中聲明的标準庫函數:

namespace std {

  typedef void (*new_handler)();

  new_handler set_new_handler(new_handler p) throw();

}

就像你能夠看到的,new_handler 是一個指針的 typedef,這個指針指向不取得和傳回任何東西的函數,而 set_new_handler 是一個取得和傳回一個 new_handler 的函數。(set_new_handler 的聲明的結尾處的 "throw()" 是一個 exception specification(異正常範)。它基本上是說這個函數不會抛出任何異常,盡管真相更有趣一些。關于細節,參見 Item 29。)

set_new_handler 的形參是一個指向函數的指針,這個函數是 operator new 無法配置設定被請求的記憶體時應該調用的。set_new_handler 的傳回值是一個指向函數的指針,這個函數是 set_new_handler 被調用前有效的目标。

你可以像這樣使用 set_new_handler:

// function to call if operator new can't allocate enough memory

void outOfMem()

{

  std::cerr << "Unable to satisfy request for memory\n";

  std::abort();

}

int main()

{

  std::set_new_handler(outOfMem);

  int *pBigDataArray = new int[100000000L];

  ...

}

如果 operator new 不能為 100,000,000 個整數配置設定空間,outOfMem 将被調用,而程式将在發出一個錯誤資訊後中止。(順便說一句,考慮如果在寫這個錯誤資訊到 cerr... 的過程中記憶體必須被動态配置設定會發生什麼。)

當 operator new 不能滿足一個記憶體請求時,它反複調用 new-handler function 直到它能找到足夠的記憶體。引起這些重複調用的代碼在 Item 51 中展示,但是從這種高層次的描述已足夠推導出一個設計得好的 new-handler function 必須做到以下事情之一:

  • Make more memory available(使得更多的記憶體可用)。這可能使得 operator new 中下一次記憶體配置設定的嘗試成功。實作這一政策的一個方法是在程式啟動時配置設定一大塊記憶體,然後在 new-handler 第一次被調用時釋放它供程式使用。
  • Install a different new-handler(安裝一個不同的 new-handler)。如果目前的 new-handler 不能做到使更多的記憶體可用,或許它知道有一個不同的 new-handler 可以做到。如果是這樣,目前的 new-handler 能在它自己的位置上安裝另一個 new-handler(通過調用 set_new_handler)。operator new 下一次調用 new-handler function 時,它會得到最近安裝的那一個。(這個主線上的一個變化是讓一個 new-handler 改變它自己的行為,這樣,下一次它被調用時,可以做一些不同的事情。做到這一點的一個方法是讓 new-handler 改變能影響 new-handler 行為的 static(靜态),namespace-specific(名字空間專用)或 global(全局)的資料。)
  • Deinstall the new-handler(解除安裝 new-handler),也就是,将空指針傳給 set_new_handler。沒有 new-handler 被安裝,當記憶體配置設定沒有成功時,operator new 抛出一個異常。
  • Throw an exception(抛出一個異常),類型為 bad_alloc 或繼承自 bad_alloc 的其它類型。這樣的異常不會被 operator new 捕獲,是以它們将被傳播到發出記憶體請求的地方。
  • Not return(不再傳回),典型情況下,調用 abort 或 exit。

這些選擇使你在實作 new-handler functions 時擁有極大的彈性。

有時你可能希望根據被配置設定 object 的不同,用不同的方法處理記憶體配置設定的失敗:

class X {

public:

  static void outOfMemory();

  ...

};

class Y {

public:

  static void outOfMemory();

  ...

};

X* p1 = new X;                        // if allocation is unsuccessful,

                                      // call X::outOfMemory

Y* p2 = new Y;                        // if allocation is unsuccessful,

                                      // call Y::outOfMemory

C++ 沒有對 class-specific new-handlers 的支援,但是它也不需要。你可以自己實作這一行為。你隻要讓每一個 class 提供 set_new_handler 和 operator new 的它自己的版本即可。class 的 set_new_handler 允許客戶為這個 class 指定 new-handler(正像standard set_new_handler 允許客戶指定global new-handler)。class 的 operator new 確定當為 class objects 配置設定記憶體時,class-specific new-handler 代替 global new-handler 被使用。

假設你要為 Widget class 處理記憶體配置設定失敗。你就必須清楚當 operator new 不能為一個 Widget object 配置設定足夠的記憶體時所調用的函數,是以你需要聲明一個 new_handler 類型的 static member(靜态成員)指向這個 class 的 new-handler function。Widget 看起來就像這樣:

class Widget {

public:

  static std::new_handler set_new_handler(std::new_handler p) throw();

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

private:

  static std::new_handler currentHandler;

};

static class members(靜态類成員)必須在 class 定義外被定義(除非它們是 const 而且是 integral ——參見 Item 2),是以:

std::new_handler Widget::currentHandler = 0;    // init to null in the class

                                                // impl. file

Widget 中的 set_new_handler 函數會儲存傳遞給它的任何指針,而且會傳回前次調用時被儲存的任何指針,這也正是 set_new_handler 的标準版本所做的事情:

std::new_handler Widget::set_new_handler(std::new_handler p) throw()

{

  std::new_handler oldHandler = currentHandler;

  currentHandler = p;

  return oldHandler;

}

最終,Widget 的 operator new 将做下面這些事情:

  1. 以 Widget 的 error-handling function 為參數調用 standard set_new_handler。這樣将 Widget 的new-handler 安裝為 global new-handler。
  2. 調用 global operator new 進行真正的記憶體配置設定。如果配置設定失敗,global operator new 調用 Widget 的 new-handler,因為那個函數剛才被安裝為 global new-handler。如果 global operator new 最後還是無法配置設定記憶體,它會抛出一個 bad_alloc exception。在此情況下,Widget 的 operator new 必須恢複原來的 global new-handler,然後傳播那個 exception。為了確定原來的 new-handler 總能被恢複,Widget 将 global new-handler 作為一種資源對待,并遵循 Item 13 的建議,使用 resource-managing objects(資源管理對象)來預防 resource leaks(資源洩漏)。
  3. 如果 global operator new 能夠為一個 Widget object 配置設定足夠的記憶體,Widget 的 operator new 傳回一個指向被配置設定記憶體的指針。object 的用于管理 global new-handler 的 destructor(析構函數)自動将 global new-handler 恢複到調用 Widget 的 operator new 之前的狀态。

以下就是你如何在 C++ 中表達這所有的事情。我們以 resource-handling class 開始,組成部分中除了基本的 RAII 操作(在構造過程中獲得資源并在析構過程中釋放)(參見 Item 13),沒有更多的東西:

class NewHandlerHolder {

public:

  explicit NewHandlerHolder(std::new_handler nh)    // acquire current

  :handler(nh) {}                                   // new-handler

  ~NewHandlerHolder()                               // release it

  { std::set_new_handler(handler); }

private:

  std::new_handler handler;                         // remember it

  NewHandlerHolder(const NewHandlerHolder&);        // prevent copying

  NewHandlerHolder&                                 // (see Item 14)

   operator=(const NewHandlerHolder&);

};

這使得 Widget 的 operator new 的實作非常簡單:

void * Widget::operator new(std::size_t size) throw(std::bad_alloc)

{

  NewHandlerHolder                              // install Widget's

   h(std::set_new_handler(currentHandler));     // new-handler

  return ::operator new(size);                  // allocate memory

                                                // or throw

}                                               // restore global

                                                // new-handler

Widget 的客戶像這樣使用它的 new-handling capabilities(處理 new 的能力):

void outOfMem();                   // decl. of func. to call if mem. alloc.

                                   // for Widget objects fails

Widget::set_new_handler(outOfMem); // set outOfMem as Widget's

                                   // new-handling function

Widget *pw1 = new Widget;          // if memory allocation

                                   // fails, call outOfMem

std::string *ps = new std::string; // if memory allocation fails,

                                   // call the global new-handling

                                   // function (if there is one)

Widget::set_new_handler(0);        // set the Widget-specific

                                   // new-handling function to

                                   // nothing (i.e., null)

Widget *pw2 = new Widget;          // if mem. alloc. fails, throw an

                                   // exception immediately. (There is

                                   // no new- handling function for

                                   // class Widget.)

無論 class 是什麼,實作這個方案的代碼都是一樣的,是以在其它地方重用它就是一個合理的目标。使它成為可能的一個簡單方法是建立一個 "mixin-style" base class(“混合風格”基類),也就是說,一個設計為允許 derived classes(派生類)繼承一個單一特定能力(在目前情況下,就是設定一個 class-specific new-handler 的能力)的 base class(基類)。然後把這個 base class(基類)轉化為一個 template(模闆),以便于你得到針對每一個 inheriting class(繼承來的類)的 class data 的不同拷貝。

這個設計的 base class(基類)部分讓 derived classes(派生類)繼承它們全都需要的 set_new_handler 和 operator new functions,而這個設計 template(模闆)部分確定每一個 inheriting class(繼承來的類)得到一個不同的 currentHandler data member(資料成員)。這聽起來可能有點複雜,但是代碼看上去可靠而且熟悉。實際上,僅有的真正不同是它現在可以用在任何需要它的 class 之上:

template<typename T>              // "mixin-style" base class for

class NewHandlerSupport{          // class-specific set_new_handler

public:                           // support

  static std::new_handler set_new_handler(std::new_handler p) throw();

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

  ...                             // other versions of op. new —

                                  // see Item 52

private:

  static std::new_handler currentHandler;

};

template<typename T>

std::new_handler

NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()

{

 std::new_handler oldHandler = currentHandler;

 currentHandler = p;

 return oldHandler;

}

template<typename T>

void* NewHandlerSupport<T>::operator new(std::size_t size)

  throw(std::bad_alloc)

{

  NewHandlerHolder h(std::set_new_handler(currentHandler));

  return ::operator new(size);

}

// this initializes each currentHandler to null

template<typename T>

std::new_handler NewHandlerSupport<T>::currentHandler = 0;

有了這個 class template(類模闆),為 Widget 增加 set_new_handler 支援就很容易了:Widget 隻需要從 NewHandlerSupport<Widget> 繼承即可。(可能看起來很奇特,但是下面我将解釋更多的細節。)

class Widget: public NewHandlerSupport<Widget> {

  ...                          // as before, but without declarations for

};                             // set_new_handler or operator new

這些就是 Widget 為了提供一個 class-specific set_new_handler 所需要做的全部。

但是也許你依然在為 Widget 從 NewHandlerSupport<Widget> 繼承而煩惱。如果是這樣,當你注意到 NewHandlerSupport template 從來沒有用到它的 type parameter T 時,你可能會更加煩惱。它不需要那樣做。我們需要的全部就是為每一個從 NewHandlerSupport 繼承的 class 提供一份不同的 NewHandlerSupport ——特别是它的 static data member(靜态資料成員)currentHandler ——的拷貝。template parameter T 隻是為了将一個 inheriting class 同另一個區分開來。template 機制自己自動地為每一個被執行個體化的 NewHandlerSupport 中的 T 生成一個 currentHandler 的拷貝。

對于 Widget 從一個把 Widget 當作一個 type parameter(類型參數)的 templatized base class(模闆化基類)繼承,如果這個概念把你弄得有點糊塗,不必難受。它最開始對每一個人都有這種影響。然而,它發展成如此有用的一項技術,它有一個名字,雖然它正常看上去所反映的事實并不是他們第一次看到它的樣子。它被稱作 curiously recurring template pattern(奇特的遞歸模闆模式) (CRTP)。真的。

在這一點上,我發表了一篇文章建議一個更好的名字叫做 "Do It For Me",因為當 Widget 從 NewHandlerSupport<Widget> 繼承時,它其實是在說:“我是 Widget,而我要從針對 Widget 的 NewHandlerSupport class 繼承。”沒有人使用我提議的名字(甚至是我自己),但是把 CRTP 考慮成說 "do it for me" 的一種方式也許會幫助你了解 templatized inheritance(模闆化繼承)在做些什麼。

像 NewHandlerSupport 這樣的 templates 使得為任何有需要的 class 添加一個 class-specific new-handler 變得易如反掌。然而,mixin-style inheritance(混合風格繼承)總是會導緻 multiple inheritance(多繼承)的話題,而在我們沿着這條路走下去之前,你需要閱讀 Item 40。

直到 1993 年,C++ 還要求 operator new 不能配置設定被請求的記憶體時要傳回 null。operator new 現在則被指定抛出一個 bad_alloc exception,但是很多 C++ 程式是在編譯器開始支援這個修訂标準之前寫成的。C++ 标準化委員會不想遺棄這些 test-for-null(檢驗是否為 null)的代碼基礎,是以他們提供了 operator new 的另一種可選形式,用以提供傳統的 failure-yields-null(失敗導緻 null)的行為。這些形式被稱為 "nothrow" 形式,這在一定程度上是因為它們在使用 new 的地方使用了 nothrow objects(定義在頭檔案 <new> 中):

class Widget { ... };

Widget *pw1 = new Widget;                 // throws bad_alloc if

                                          // allocation fails

if (pw1 == 0) ...                         // this test must fail

Widget *pw2 =new (std::nothrow) Widget;   // returns 0 if allocation for

                                          // the Widget fails

if (pw2 == 0) ...                         // this test may succeed

對于異常,nothrow new 提供了比最初看上去更少的強制保證。在表達式 "new (std::nothrow) Widget" 中,發生了兩件事。首先,operator new 的 nothrow 版本被調用來為一個 Widget object 配置設定足夠的記憶體。如果這個配置設定失敗,衆所周知,operator new 傳回 null pointer。然而,如果它成功了,Widget constructor 被調用,而在此刻,所有打的賭都失效了。Widget constructor 能做任何它想做的事。它可能自己 new 出來一些記憶體,而如果它這樣做了,它并沒有被強迫使用 nothrow new。那麼,雖然在 "new (std::nothrow) Widget" 中調用的 operator new 不會抛出,Widget constructor 卻可以。如果它這樣做了,exception 像往常一樣被傳播。結論?使用 nothrow new 隻能保證 operator new 不會抛出,不能保證一個像 "new (std::nothrow) Widget" 這樣的表達式絕不會導緻一個 exception。在所有的可能性中,你最好絕不需要 nothrow new。

無論你是使用 "normal"(也就是說,exception-throwing)new,還是它的稍微有些矮小的堂兄弟,了解 new-handler 的行為是很重要的,因為它可以用于兩種形式。

Things to Remember

  • set_new_handler 允許你指定一個當記憶體配置設定請求不能被滿足時可以被調用的函數。
  • nothrow new 作用有限,因為它僅适用于記憶體配置設定,随後的 constructor 調用可能依然會抛出 exceptions。