Effective C++ 读书笔记 Item 49 new handler的行为

朱雀 2020-07-07 14:48 783阅读 0赞

new申请内存失败时会抛出"bad alloc"异常,此前会调用一个由std::set_new_handler()指定的错误处理函数(”new-handler”)。

set_new_handler()

“new-handler”函数通过std::set_new_handler()来设置,std::set_new_handler()定义在<new>中:

  1. namespace std{
  2. typedef void (*new_handler)();
  3. new_handler set_new_handler(new_handler p) throw();
  4. }

throw()是一个异常声明,表示不抛任何异常。例如void func() throw(Exception1, Exception2)表示func可能会抛出Exception1Exception2两种异常。

set_new_handler()的使用也很简单:

  1. void outOfMem(){
  2. std::cout<<"Unable to alloc memory";
  3. std::abort();
  4. }
  5. int main(){
  6. std::set_new_handler(outOfMem);
  7. int *p = new int[100000000L];
  8. }

当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:

  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 current;
  7. };
  8. // 静态成员需要定义在类的外面
  9. std::new_handler Widget::current = 0;
  10. std::new_handler Widget::set_new_handler(std::new_handler p) throw(){
  11. std::new_handler old = current;
  12. current = p;
  13. return old;
  14. }

关于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类:

  1. class NewHandlerHolder{
  2. public:
  3. explicit NewHandlerHolder(std::new_handler nh): handler(nh){}
  4. ~NewHandlerHolder(){ std::set_new_handler(handler); }
  5. private:
  6. std::new_handler handler;
  7. NewHandlerHolder(const HandlerHolder&); // 禁用拷贝构造函数
  8. const NewHandlerHolder& operator=(const NewHandlerHolder&); // 禁用赋值运算符
  9. };

然后Widget::operator new的实现其实非常简单:

  1. void * Widget::operator new(std::size_t size) throw(std::bad_alloc){
  2. NewHandlerHolder h(std::set_new_handler(current));
  3. return ::operator new(size); // 调用全局的new,抛出异常或者成功
  4. } // 函数调用结束,原有错误处理函数恢复

使用Widget::operator new

客户使用Widget的方式也符合基本数据类型的惯例:

  1. void outOfMem();
  2. Widget::set_new_handler(outOfMem);
  3. Widget *p1 = new Widget; // 如果失败,将会调用outOfMem
  4. string *ps = new string; // 如果失败,将会调用全局的 new-handling function,当然如果没有的话就没有了
  5. Widget::set_new_handler(0); // 把Widget的异常处理函数设为空
  6. Widget *p2 = new Widget; // 如果失败,立即抛出异常

通用基类

仔细观察上面的代码,很容易发现自定义”new-handler”的逻辑其实和Widget是无关的。我们可以把这些逻辑抽取出来作为一个模板基类:

  1. template<typename T>
  2. class NewHandlerSupport{
  3. public:
  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. private:
  7. static std::new_handler current;
  8. };
  9. template<typename T>
  10. std::new_handler NewHandlerSupport<T>::current = 0;
  11. template<typename T>
  12. std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw(){
  13. std::new_handler old = current;
  14. current = p;
  15. return old;
  16. }
  17. template<typename T>
  18. void * NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc){
  19. NewHandlerHolder h(std::set_new_handler(current));
  20. return ::operator new(size);
  21. }

有了这个模板基类后,给Widget添加”new-handler”支持只需要public继承即可:

  1. class Widget: public NewHandlerSupport<Widget>{ ... };

其实NewHandlerSupport的实现和模板参数T完全无关,添加模板参数是因为handler是静态成员,这样编译器才能为每个类型生成一个handler实例。

nothrow new

1993年之前C++的operator new在失败时会返回null而不是抛出异常。如今的C++仍然支持这种nothrow的operator new

  1. Widget *p1 = new Widget; // 失败时抛出 bad_alloc 异常
  2. assert(p1 != 0); // 这总是成立的
  3. Widget *p2 = new (std::nothrow) Widget;
  4. if(p2 == 0) ... // 失败时 p2 == 0

“nothrow new” 只适用于内存分配错误。而构造函数也可以抛出的异常,这时它也不能保证是new语句是”nothrow”的。

发表评论

表情:
评论列表 (有 0 条评论,783人围观)

还没有评论,来说两句吧...

相关阅读