50.了解new-handler

前言

 
当operator new无法满足某一个内存分配需求时,它会抛出异常,在其抛出异常之前,它会先调用一个客户制定的错误处理函数,一个所谓的new-handler。(实际上更复杂一些)


set_new_handler

 
为了指定这个“用以处理内存不足”的函数,用户必须调用set_new_handler,那是声明于<new>的一个标准库函数:

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

显然,new_handler是一个函数指针的typedef,该函数没有任何参数也不返回任何东西。set_new_handler则是接受一个new_handler参数并返回一个new_handler参数。set_new_handler尾端的throw()表明该函数理论上不会抛出异常,详见Effective C++ 30。

set_new_handler的使用

set_new_handler的参数是一个指针,指向operator new无法分配足够内存时需要被调用的函数,其返回值指向set_new_handler被调用前正在执行的那个函数。具体使用方法如下:

1
2
3
4
5
6
7
8
9
void outOfMem(){
cerr << “Unable to satisfy request for memory\n”;
abort();//使程序异常中止

int main(){
set_new_handler(outOfMem);
int *pBigDataArray = new int [10000000];
...
}

就本例而言,如果无法创建如此大的数组,则outofmem就会被调用,于是程序在发出信息后abort。
当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。


new-handler设计要求

 
一个设计良好的new-handler可以提供如下功能:

  • 让更多内存可被调用
    一种实现方法是程序一开始执行时就分配一大块内存,而后当new_handler第一次被调用时,将它释放给程序使用。
  • 安装另一个new-handler
    如果当前无法获取更多内存,但又明确知道其他某个new-handler有这个能力,就应该主动使用set-handler进行替换。
  • 卸载new-handler
    将nullptr传递给set-new-handler。如果new-handler为nullptr,operator new会在分配内存不成功时抛出异常。
  • 抛出bad_alloc(或派生自bad_alloc)的异常
    这样的异常不会被operator new捕捉,因此会被传播到内存索求处。
  • 不返回
    直接调用abort或者exit。

针对class定制new-handler

 
或许你会希望不同的class以不同的new-handler来处理内存分配失败情况:

1
2
3
4
5
6
7
8
9
10
11
12
class X{
public:
static void outOfMemory();
...
};
class Y{
public:
static void outOfMemory();
...
};
X *px = new X;//分配失败时调用X::outOfMemory
Y *py = new Y;//分配失败时调用Y::outOfMemory

但c++并不支持class专属的new-handler.但这无关紧要,只需要令每一个class提供自己的set_new_handler和operator new即可。

问题实例

假设当前我们准备处理Widget class的内存分配失败情况。首先必须定义Widget专有的new—handler,于是我们先声明一个类型为new handler的static成员:

1
2
3
4
5
6
7
class Widget{
public:
static new_handler set_new_handler(new_handler p) throw();
static void* operator new(size_t size) throw(bad_alloc);
private:
static new_handler currentHandler;
};

Static成员必须在class定义式之外被定义(除非是const的int,详见Effective C++ 3),因此:
1
new_handler Widget::currentHandler = nullptr;

Widget中的set_new_handler函数会将其获得的new_handler保存,然后返回被替换的指针,如下所示:
1
2
3
4
5
new_handler Widget::set_new_handler(new_handler p) throw() {
new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}

实例剖析

Widget的operator new做了如下事情:

  1. 调用标准set_new_handler,告知A的错误处理函数。
    执行完毕后,A的new_handler被安装为global new_handler.
  2. 调用global operator new,执行实际内存分配。
    如果失败,global operator new会调用Widget的new_handler,因为它刚才被安装为global.如果最终确实无法分配足够内存,会抛出一个bad_alloc的异常。此时A的operator new必须恢复global new_handler,然后再传播该异常。为了保证原本的new-handler总是正确安装,应该将global new_handler视为资源,使用RAII.
  3. 如果global operator new能够分配足够的内存,A的operator new会返回一个指向分配所得的指针。
    Widget的析构函数会管理global new_handler,它会自动将Widget的operator new被调用前的global new_handler恢复。

解决方案

我们首先从资源管理类开始:

1
2
3
4
5
6
7
8
9
10
class NewHandlerHolder{
public:
explicit NewHandlerHolder(new_handler nh):handler(nh) {}
~NewHandlerHolder() {set_new_handler(handler);}//恢复global new_handler
private:
new_handler handler;//记录
//阻止copy 详见Effective C++ 15
NewHandlerHolder(const NewHandlerHolder&);
NewHandlerHolder& operator=(const NewHandlerHolder&);
};

因此,有Widget::operator new实现如下:
1
2
3
4
5
void* Widget::operator new(size_t size) throw(bad_alloc){
NewHandlerHolder h(set_new_handler(currentHandler));//安装Widget::new_handler
return ::operator new(size);//分配内存或抛出异常
//资源管理对象析构 global new_handler恢复
}

Widget使用方案如下:
1
2
3
4
void outOfMem();//声明内存分配失败处理函数
Widget::set_new_handler(outOfMem);
Widget* pw = new Widget;//分配失败调用outOfMem
string* ps = new string;//分配失败调用global new_handler


设计模板解决方案

 
我们通过观察可以发现,这一Widget::set_new_handler这一实现方案并不因class发生改变而改变,因此可以对实现复用。一种简单的做法是建立一个“mixin”风格的base class,这种bc允许dc继承某种特定能力—本实例中中是“设定class专属new_handler的能力”。然后将这个base class转换为template,这样每一个dc都将获得实体互异的class data复件。
该设计得bc让dc继承它们所需的set_new_handler与operator new,template则是为了保证每一个dc拥有一个实体互异的currentHandler成员变量。具体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template<typename T>
class NewHandlerHolderSupport{
public:
static new_handler set_new_handler(new_handler p) throw();
static void* operator new(size_t size) throw(bad_alloc);
...
private:
static new_handler currentHandler;
};
template<typename T>
new_handler NewHandlerSupport<T>::set_new_handler(new_handler p) throw(){
new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template<typename T>
void* NewHandlerSupport<T>::operator new(size_t size) throw(bad_alloc){
NewHandlerHolder h(set_new_handler(currentHandler));
return ::operator new(size);
}
template<typename T>
new_handler NewHandlerSupport<T>::currentHandler = nullptr;//static对象定义

有了这个class template,为Widget添加set_new_handler与operator new简直轻而易举:只需要令A继承自NewHandlerSupport<Widget>即可:
1
class Widget::public NweHandlerSupport<Widget> {...}

但是这种继承似乎不合逻辑,它们之间根本就不是is-a的关系,而且template的参数T根本没有被使用过。实际上它本来就没用,它只是用来区分不同的dc,template机制会为每一个dc自动生成一个static currentHandler。

class A继承自一个模版化的base class ,而后者又以A作为模板参数,这种技术被称为CRTP(curiously recuring template pattern)。


总结

  1. set_new_handler允许客户指定一个函数,在内存无法分配时被调用。
  2. Nothrow new只能保证内存分配时不会抛出异常,但是后续构造函数调用还是可能会抛出异常。