Item 49: Understand the behavior of the new-handler.
new
申請內存失敗時會拋出"bad alloc"
異常,此前會調用一個由std::set_new_handler()
指定的錯誤處理函數(”new-handler”)。
“new-handler”函數通過std::set_new_handler()
來設置,std::set_new_handler()
定義在
中:
namespace std{
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}
throw()
是一個異常聲明,表示不拋任何異常。例如void func() throw(Exception1, Exception2)
表示func
可能會拋出Exception1
,Exception2
兩種異常。
set_new_handler()
的使用也很簡單:
void outOfMem(){
std::cout<<"Unable to alloc memory";
std::abort();
}
int main(){
std::set_new_handler(outOfMem);
int *p = new int[100000000L];
}
當new
申請不到足夠的內存時,它會不斷地調用outOfMem
。因此一個良好設計的系統中outOfMem
函數應該做如下幾件事情之一:
null
給set_new_handler
即可;bad_alloc
(或它的子類)異常;abort
或者exit
。
std::set_new_handler
設置的是全局的bad_alloc
的錯誤處理函數,C++並未提供類型相關的bad_alloc
異常處理機制。 但我們可以重載類的operator new
,當創建對象時暫時設置全局的錯誤處理函數,結束後再恢復全局的錯誤處理函數。
比如Widget
類,首先需要聲明自己的set_new_handler
和operator new
:
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 current;
};
// 靜態成員需要定義在類的外面
std::new_handler Widget::current = 0;
std::new_handler Widget::set_new_handler(std::new_handler p) throw(){
std::new_handler old = current;
current = p;
return old;
}
關於
abort
,exit
,terminate
的區別:abort
會設置程序非正常退出,exit
會設置程序正常退出,當存在未處理異常時C++會調用terminate
, 它會回調由std::set_terminate
設置的處理函數,默認會調用abort
。
最後來實現operator new
,該函數的工作分為三個步驟:
std::set_new_handler
,把Widget::current
設置為全局的錯誤處理函數;operator new
來分配真正的內存;Widget::current
將會拋出異常;Widget::current
,並安裝調用Widget::operator new
之前的全局錯誤處理函數。
我們通過RAII類來保證原有的全局錯誤處理函數能夠恢復,讓異常繼續傳播。關於RAII可以參見Item 13。 先來編寫一個保持錯誤處理函數的RAII類:
class NewHandlerHolder{
public:
explicit NewHandlerHolder(std::new_handler nh): handler(nh){}
~NewHandlerHolder(){ std::set_new_handler(handler); }
private:
std::new_handler handler;
NewHandlerHolder(const HandlerHolder&); // 禁用拷貝構造函數
const NewHandlerHolder& operator=(const NewHandlerHolder&); // 禁用賦值運算符
};
然後Widget::operator new
的實現其實非常簡單:
void * Widget::operator new(std::size_t size) throw(std::bad_alloc){
NewHandlerHolder h(std::set_new_handler(current));
return ::operator new(size); // 調用全局的new,拋出異常或者成功
} // 函數調用結束,原有錯誤處理函數恢復
客戶使用Widget
的方式也符合基本數據類型的慣例:
void outOfMem();
Widget::set_new_handler(outOfMem);
Widget *p1 = new Widget; // 如果失敗,將會調用outOfMem
string *ps = new string; // 如果失敗,將會調用全局的 new-handling function,當然如果沒有的話就沒有了
Widget::set_new_handler(0); // 把Widget的異常處理函數設為空
Widget *p2 = new Widget; // 如果失敗,立即拋出異常
仔細觀察上面的代碼,很容易發現自定義”new-handler”的邏輯其實和Widget
是無關的。我們可以把這些邏輯抽取出來作為一個模板基類:
template
class NewHandlerSupport{
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 current;
};
template
std::new_handler NewHandlerSupport::current = 0;
template
std::new_handler NewHandlerSupport::set_new_handler(std::new_handler p) throw(){
std::new_handler old = current;
current = p;
return old;
}
template
void * NewHandlerSupport::operator new(std::size_t size) throw(std::bad_alloc){
NewHandlerHolder h(std::set_new_handler(current));
return ::operator new(size);
}
有了這個模板基類後,給Widget
添加”new-handler”支持只需要public繼承即可:
class Widget: public NewHandlerSupport{ ... };
其實NewHandlerSupport
的實現和模板參數T
完全無關,添加模板參數是因為handler
是靜態成員,這樣編譯器才能為每個類型生成一個handler
實例。
1993年之前C++的operator new
在失敗時會返回null
而不是拋出異常。如今的C++仍然支持這種nothrow的operator new
:
Widget *p1 = new Widget; // 失敗時拋出 bad_alloc 異常
assert(p1 != 0); // 這總是成立的
Widget *p2 = new (std::nothrow) Widget;
if(p2 == 0) ... // 失敗時 p2 == 0
“nothrow new” 只適用於內存分配錯誤。而構造函數也可以拋出的異常,這時它也不能保證是new
語句是”nothrow”的。