读书笔记 effective c++ Item 49 理解new-handler的行为
1. new-handler介紹
當操作符new不能滿足內存分配請求的時候,它就會拋出異常。很久之前,它會返回一個null指針,一些舊的編譯器仍然會這么做。你仍然會看到這種舊行為,但是我會把關于它的討論推遲到本條款結束的時候。
1.1 調用set_new_handler來指定全局new-handler
在operator new由于不能滿足內存分配要求而拋出異常之前,它會調用一個客戶指定的叫做new-handler的錯誤處理函數。(這也不是完全正確的。Operator new的真正行為更加復雜。詳細內容在Item 51中描述。)為了指定內存溢出處理(out-of-memory-handling)函數,客戶可以調用set_new_handler函數,這個標準庫函數被聲明在<new>中:
1 namespace std { 2 typedef void (*new_handler)(); 3 new_handler set_new_handler(new_handler p) throw(); 4 }?
正如你所看到的,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之前調用的new_handler函數(也就是在new_handler被替換之前的函數)。
你可以像下面這樣使用set_new_handler:
1 // function to call if operator new can’t allocate enough memory 2 void outOfMem() 3 { 4 std::cerr << "Unable to satisfy request for memory\n"; 5 std::abort(); 6 } 7 8 int main() 9 { 10 std::set_new_handler(outOfMem); 11 int *pBigDataArray = new int[100000000L]; 12 ... 13 }?
如果operaotr new無法為100,000,000個整數分配內存,就會調用outOfMem,也就是輸出一個error信息之后程序終止(abort)。(順便說一下,考慮在向cerr中寫入error信息期間如果必須動態的分配內存會發生什么。。)
1.2 如何設計一個良好的new-handler函數
當operator new不能滿足一個內存請求的時候,它會反復調用new-handler函數直到它發現有足夠的內存可以分配了。引起這些函數被反復調用的代碼在Item 51中可以找到,但是這種高級別的描述信息足夠讓我們得出結論:一個設計良好的new-handler函數必須能夠做到如下幾點。
- 提供更多的可被使用的內存。這可以保證下次在operator new內部嘗試分配內存時能夠成功。實現這個策略的一種方法是在程序的開始階段分配一大塊內存,然后在第一次調用new-handler的時候釋放它。
- 安裝一個不同的new-handler。如果當前的new-handler不能夠為你提供更多的內存,可能另外一個new-handler可以。如果是這樣,可以在當前的new-handler的位置上安裝另外一個new-handler(通過調用set_new_handler)。下次operator new調用new-handler函數的時候,它會調用最近安裝的。(這個主題的一個變種是一個使用new_handler來修改它自己的行為,所以在下次觸發這個函數的時候,它就會做一些不同的事情。達到這個目的的一個方法是讓new_handler修改影響new-handler行為的static數據,命名空間數據或者全局數據。)
- 卸載new-handler,也就是為set_new_handler傳遞null指針。如果沒有安裝new-handler,operator ?new在內存分配失敗的時候會拋出異常。
- 沒有返回值,調用abort或者exit。
這些選擇讓你在實現new-handler的時候有相當大的靈活性。
2. 為特定類指定new-handler
有時候你想用不同方式來處理內存分配失敗,這依賴于需要分配內存的對象所屬的類:
1 class X { 2 public: 3 static void outOfMemory(); 4 ... 5 }; 6 class Y { 7 public: 8 static void outOfMemory(); 9 ... 10 }; 11 X* p1 = new X; // if allocation is unsuccessful, 12 // call X::outOfMemory 13 Y* p2 = new Y; // if allocation is unsuccessful, 14 // call Y::outOfMemory?
C++沒有為類提供指定的new-handlers,但也不需要。你可以自己實現這種行為。你可以使每個類提供自己版本的set_new_handler和operator new。類中的set_new_handler允許客戶為類提供new_handler(就像標準的set_new_handler允許客戶指定全局的new-handler一樣)。類的operator new確保為類對象分配內存時,會使用其指定的new-handler來替代全局new-handler。
2.1 在類中聲明static new_handler成員
假設你想對Widget類對象的內存分配失敗做一下處理。當operator new不能為Widget對象分配足夠的內存的時候你必須跟蹤一下函數調用過程,所以你要聲明一個類型為new_handler的static成員,來指向這個類的new-handler函數。Widget將會是下面這個樣子:
1 class Widget { 2 public: 3 static std::new_handler set_new_handler(std::new_handler p) throw(); 4 static void* operator new(std::size_t size) throw(std::bad_alloc); 5 private: 6 static std::new_handler currentHandler; 7 };?
靜態類成員必須在類外部定義(除非他們是const整型,見Item 2),所以:
1 std::new_handler Widget::currentHandler = 0; // init to null in the class 2 // impl. File?
Widget中的set_new_handler函數會把傳遞進去的指針(所指向的new-handler函數)保存起來,并且會返回調用set_new_handler之前所保存的指針。這也是標準版本set_new_handler的做法:
1 std::new_handler Widget::set_new_handler(std::new_handler p) throw() 2 { 3 std::new_handler oldHandler = currentHandler; 4 currentHandler = p; 5 return oldHandler; 6 }?
2.2 重新定義operator new
最后,Widget的operator new將會做下面的事情:
這里我們以資源處理(resource-handling)類開始,只包含基本的RAII處理操作,包括在構造時獲取資源和在在析構時釋放資源(Item 13):
1 class NewHandlerHolder { 2 public: 3 explicit NewHandlerHolder(std::new_handler nh) // acquire current 4 : handler(nh) {} // new-handler 5 6 ~NewHandlerHolder() // release it 7 8 { std::set_new_handler(handler); } 9 10 private: 11 12 13 14 std::new_handler handler; // remember it 15 16 NewHandlerHolder(const NewHandlerHolder&); // prevent copying 17 18 19 20 NewHandlerHolder& // (see Item 14) 21 22 operator=(const NewHandlerHolder&); 23 24 };? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
?這會使得Widget的operator new的實現非常簡單:
1 void* Widget::operator new(std::size_t size) throw(std::bad_alloc) 2 { 3 NewHandlerHolder // install Widget’s 4 h(std::set_new_handler(currentHandler)); // new-handler 5 6 return ::operator new(size); // allocate memory 7 // or throw 8 9 } // restore global 10 // new-handler 11 12 13 14 void outOfMem(); // decl. of func. to call if mem. alloc. 15 // for Widget objects fails 16 17 Widget::set_new_handler(outOfMem); // set outOfMem as Widget’s 18 // new-handling function 19 20 Widget *pw1 = new Widget; // if memory allocation 21 // fails, call outOfMem 22 23 std::string *ps = new std::string; // if memory allocation fails, 24 // call the global new-handling 25 // function (if there is one) 26 27 Widget::set_new_handler(0); // set the Widget-specific 28 // new-handling function to 29 // nothing (i.e., null) 30 31 Widget *pw2 = new Widget; // if mem. alloc. fails, throw an 32 // exception immediately. (There is 33 // no new- handling function for 34 // class Widget.)?
2.3 將NewHandlerHolder轉換為模板
不管在什么類中,實現的這個主題的代碼都是一樣的,所以我們可以為其設一個合理的目標,就是代碼能夠在其他地方重用。達到這個目標的一個簡單方法是創建一個“混合風格(mixin-style)”的基類,也就是設計一個基類,允許派生類繼承單一特定的能力——在這個例子中,這種能力就是為類指定new-handler。然后將基類變為一個模板,于是你可以為每個繼承類獲得一份不同的類數據的拷貝。
這個設計的基類部分使得派生類能夠繼承它們都需要的set_new_handler和operator new函數,同時設計的模板部分確保每個繼承類獲得一個不同的currentHandler數據成員。說起來有些復雜,但是代碼看上去很熟悉。事實上,唯一真正不一樣的是現在任何類都能夠獲得這個功能:
1 template<typename T> // “mixin-style” base class for 2 class NewHandlerSupport { // class-specific set_new_handler 3 public: // support 4 static std::new_handler set_new_handler(std::new_handler p) throw(); 5 static void* operator new(std::size_t size) throw(std::bad_alloc); 6 ... // other versions of op. new — 7 // see Item 52 8 private: 9 static std::new_handler currentHandler; 10 }; 11 template<typename T> 12 std::new_handler 13 NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw() 14 { 15 std::new_handler oldHandler = currentHandler; 16 currentHandler = p; 17 return oldHandler; 18 } 19 template<typename T> 20 void* NewHandlerSupport<T>::operator new(std::size_t size) 21 throw(std::bad_alloc) 22 { 23 NewHandlerHolder h(std::set_new_handler(currentHandler)); 24 return ::operator new(size); 25 } 26 // this initializes each currentHandler to null 27 template<typename T> 28 std::new_handler NewHandlerSupport<T>::currentHandler = 0;?
有了這個類模板之后,向Widget中添加set_new_handler支持就變得容易了:Widget只需要繼承自NewHandlerSupport<Widget>。(這可能看上去比較獨特,接下來我會進行詳細的解釋。)
1 class Widget: public NewHandlerSupport<Widget> { 2 ... // as before, but without declarations for 3 4 }; // set_new_handler or operator new?
這是Widget提供一個特定的set_new_handler需要做的所有事情。
但是對于Widget繼承自NewHandlerSupport<Widget>,你可能還是有些不安。如果是這樣,當你注意到NewHandlerSupport模板永遠不會使用類型參數T之后你的不安可能會加劇。你沒有必要這樣。對于每個繼承自NewHandlerSupport的類來說,我們所有需要的是一份不同的NewHandlerSupport的拷貝——特別是靜態數據成員currentHandler的不同拷貝。模板機制自身會為每個T自動生成currentHandler的一份拷貝,NewHandlerSupport使用這個T來進行實例化。
對于Widget繼承自一個使用Widget作為類型參數的模板基類來說,如果這個概念讓你感覺眩暈,不要感覺不好。每個人看到開始看到它的時候都會有這種感覺。但是,它是非常有用的技術,它有一個名字,這個名字如果這個概念一樣,第一次看到它的人沒有人會感覺它很自然,它叫做怪異的循環模板模式(curiously recurring template pattern CRTP)。
我曾經寫過一遍文章建議為它起一個更好的名字:do it for me,因為當Widget繼承自NewHandlerSupport<Widget>,它真的像是在說:“我是Widget,我需要為Widget繼承NewHandlerSupport類“。沒有人使用我建議的名字,但是使用“do it for me”來想象一下CRTP可能會幫助你理解模板化的繼承會做什么。
有了像NewHandlerSupport這樣的模板,為任何需要new-hadler的類添加一個特定的new-handler就會變得容易。混合風格的繼承總是會將你引入多繼承的主題,在開始進入這個主題之前,你可能想讀一下Item 40。
3. Nothrow版本的new
直到1993年,當不能滿足分配內存的要求時,C++要求operator new要返回null。現在指定operator new要拋出bad_alloc異常,但是大量的C++是在編譯器支持修訂版本之前寫出來的。C++標準委員會也不想廢棄test-for-null的代碼,所以它們為operator new提供了一種替代形式,它能夠提供傳統的“失敗產生null(failure-yields-null)”行為。這些形式被叫做“nothrow”形式,某種程度上是因為他們使用了不會拋出異常的對象(定義在頭文件<new>中),new在這種情況下被使用:
1 class Widget { ... }; 2 Widget *pw1 = new Widget; // throws bad_alloc if 3 // allocation fails 4 5 if (pw1 == 0) ... // this test must fail 6 7 Widget *pw2 = new (std::nothrow) Widget; // returns 0 if allocation for 8 // the Widget fails 9 10 if (pw2 == 0) ... // this test may succeed?
nothrow版本的new不會像從表面上看起來這樣可靠,對于異常它沒有提供讓人信服的保證。對于表達式“new (std::nothrow) Widget”,會發生兩件事情。首先,通過調用nothrow版本的operator new來為一個Widget 對象分配足夠的內存。如果分配失敗了,operator new會返回null指針。然而如果分配成功了,Widget構造函數會被調用,到這個時候,就會世事難料了。Widget構造函數能夠做任何它想做的。它自己可能new一些內存,如果是這樣,并沒有強迫它使用nothrow版本的new。雖然在”new (std::nothrow) Widget”中的operator new不會拋出異常,但是Widget構造函數卻可能拋出來。如果是這樣,異常會像平時一樣傳播出去。結論是什么?使用nothrow new只能保證operator new不會拋出異常,不能保證像“new(std::nothrow) Widget”這樣的表達式不拋出異常。十有八九,你將永遠不會有使用nothrow new的需要。
不論你是使用”普通的”(也就是拋出異常的)new還是nothrow版本的new,重要的是你需要明白new-handler的行為,因為在兩種new中都會使用到它。
4. 總結
- Set_new_handler允許你在分配內存不能滿足要求的時候指定一個特定的被調用的函數。
- Nothrow new功能有限,因為它只能被應用在內存分配上;相關聯的構造函數調用可能仍然會拋出異常。
作者: HarlanC
博客地址: http://www.cnblogs.com/harlanc/
個人博客: http://www.harlancn.me/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出, 原文鏈接
如果覺的博主寫的可以,收到您的贊會是很大的動力,如果您覺的不好,您可以投反對票,但麻煩您留言寫下問題在哪里,這樣才能共同進步。謝謝!
總結
以上是生活随笔為你收集整理的读书笔记 effective c++ Item 49 理解new-handler的行为的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vCenter功能基本介绍
- 下一篇: (30个原生js挑战)原生js实现钟表