条款49:了解 new-handler 的行为
1.1 new-handler
在new操作符抛出异常以响应无法满足的内存请求之前,它会调用客户指定的错误处理函数new-handler。(实际情况比这更复杂)
namespace std {
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) noexcept;
}
#include <iostream>
// 如果new运算符不能分配足够的内存,要调用的函数
void outOfMem()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort(); //如果不终止,程序会一直尝试调用这个处理函数
}
int main()
{
std::set_new_handler(outOfMem);
for(int i=0; i<100000;i++)
{
int* pBigDataArray = new int[100000000L];
}
}
1.2 new-handler任务
设计良好的new-handler函数必须完成下列任务之一:
- 让更多内存可被使用。例如:程序一开始就分配一大块内存,而后当 new-handler 第一次被调用,将它们释还给程序使用。
- 安装另一个 new-handler。如果当前 new-handler 无法取得更多可用内存,或许它知道另外哪个 new-handler 有此能力。(通过调用set_new_handler,或通过改变全局数据改变自己的行为)
- 卸除 new-handler,也就是将 nullptr 传给 set_new_handler,这样 operator new 会在内存分配失败时抛出异常。
- 抛出 bad_alloc 或其派生的异常。这样的异常不会被 operator new 捕捉,而会被传播到内存索求处。
- 不返回,通常调用 abort 或 exit。
1.3 提出问题
有时你想根据被分配对象的类,用不同的方式处理内存分配失败:
class Widget {
public:
static std::new_handler set_new_handler(std::new_handler p) noexcept;
static void* operator new(std::size_t size);
private:
static std::new_handler currentHandler;
};
std::new_handler Widget::currentHandler = nullptr; // 初始化为空指针
std::new_handler Widget::set_new_handler(std::new_handler p) noexcept
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler nh) // 获取当前的new-handler
:handler(nh) {}
//类的set_new_handler允许客户端为类指定new-handler
~NewHandlerHolder(){std::set_new_handler(handler);}// 释放它
private:
std::new_handler handler; // 记录下来
NewHandlerHolder(const NewHandlerHolder&); // 防止拷贝
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
void* Widget::operator new(std::size_t size) throw(std::bad_alloc){
// 安装Widget的new-handler
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size); // 分配内存或抛出异常
} // 恢复全局的new-handler
Widget的客户像下面这样使用它的新处理功能:
void outOfMem(); // 函数声明
Widget::set_new_handler(outOfMem); // 将outOfMem设置为Widget的新处理函数
Widget* pw1 = new Widget; // 如果内存分配失败,调用outOfMem
std::string* ps = new std::string; //如果内存分配失败,调用全局新处理函数
Widget::set_new_handler(nullptr); // 设置Widget特定的新处理函数为空
Widget* pw2 = new Widget; // 如果内存分配失败, 立即抛出异常(Widget没有特定的新处理函数)
1.4 解决办法
不同的类,实现这个方案的代码都是相同的,所以应该能够重用它。一种简单的方法是创建一个“mixin风格”的基类,它的设计目的是允许派生类继承一个特定的功能。在本例中,即设置类专用的new-handler。然后将基类转换为模板,以便支持不同类型。
template<typename T> // “mixin分割” 基类,用于支持类特定的set_new_handler
class NewHandlerSupport {
public:
static std::new_handler set_new_handler(std::new_handler p) noexcept;
static void* operator new(std::size_t size) throw(std::bad_alloc);
... // 其他版本的operator new
private:
static std::new_handler currentHandler;
};
template<typename T>
std::new_handler
NewHandlerSupport<T>::set_new_handler(std::new_handler p) noexcept
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size)
throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
// 这会将每个currentHandler初始化为空指针
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = nullptr;
class Widget : public NewHandlerSupport<Widget> {
... // 和之前一样,但没有set_new_handler或operator new的声明
};
//这里用到了一种编程技巧, CRTP(curious recurring template pattern,奇异循环模板模式)
1.5 nothrow new
C++的另一种new运算符,提供了传统的“分配失败便返回空指针”。这被称为“nothrow”形式,部分原因是它们在使用new的地方使用了nothrow对象(定义在头文件 中)
class Widget { ... };
Widget* pw1 = new Widget; // 如果分配失败,抛出 bad_alloc
if (pw1 == 0) ... // 这个测试必然失败
Widget* pw2 = new (std::nothrow) Widget; // 如果Widget分配失败,返回空指针
if (pw2 == 0) ... // 这个测试可能成功
1.6 总结
- set_new_handler允许指定一个在无法满足内存分配请求时调用的函数。
- nothrow new的用途有限,因为它只适用于内存分配,后续的构造函数调用仍然可能抛出异常。