Item 49: Understand the behavior of the new-handler.
new
申请内存失败时会抛出"bad alloc"
异常,此前会调用一个由std::set_new_handler()
指定的错误处理函数(”new-handler”)。
set_new_handler()
“new-handler”函数通过std::set_new_handler()
来设置,std::set_new_handler()
定义在<new>
中:
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
函数应该做如下几件事情之一:
- 使更多内存可用;
- 安装一个新的”new-handler”;
- 卸载当前”new-handler”,传递
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
之前的全局错误处理函数。
重载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::operator new
客户使用Widget
的方式也符合基本数据类型的惯例:
void outOfMem();Widget::set_new_handler(outOfMem);Widget *p1 = new Widget; // 如果失败,将会调用outOfMemstring *ps = new string; // 如果失败,将会调用全局的 new-handling function,当然如果没有的话就没有了Widget::set_new_handler(0); // 把Widget的异常处理函数设为空Widget *p2 = new Widget; // 如果失败,立即抛出异常
通用基类
仔细观察上面的代码,很容易发现自定义”new-handler”的逻辑其实和Widget
是无关的。我们可以把这些逻辑抽取出来作为一个模板基类:
template<typename T>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<typename T>std::new_handler NewHandlerSupport<T>::current = 0;template<typename T>std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw(){ std::new_handler old = current; current = p; return old;}template<typename T>void * NewHandlerSupport<T>::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<Widget>{ ... };
其实NewHandlerSupport
的实现和模板参数T
完全无关,添加模板参数是因为handler
是静态成员,这样编译器才能为每个类型生成一个handler
实例。
nothrow new
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”的。
本文地址:http://harttle.com/2015/09/17/effective-cpp-49.html
0 0