读书笔记 effective c++ Item 49 理解new-handler的行为

1. new-handler介绍

当操作符new不能满足内存分配请求的时候,它就会抛出异常。很久之前,它会返回一个null指针,一些旧的编译器仍然会这么做。你仍然会看到这种旧行为,但是我会把关于它的讨论推迟到本条款结束的时候。

1.1 调用set_new_handle

来指定全局new-handler

在operator new由于不能满足内存分配要求而抛出异常之前,它会调用一个客户指定的叫做new-handler的错误处理函数。(这也不是完全正确的。Operator new的真正行为更加复杂。详细内容在Item 51中描述。)为了指定内存溢出处理(out-of-memory-handling)函数,客户可以调用set_new_handler函数,这个标准库函数被声明在中:

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的函数。(函数set_new_handler声明结束处的”throw()”是一个异常指定(exception
specification)。从本质上来说它的意思是说这个函数不会抛出任何异常,然而事实更加有意思。详细内容见Item 29。)

set_new_handler的参数是指向函数的指针,operator new会在请求的内存无法分配的情况下调用这个函数。Set_new_handler的返回值也是指向函数的指针,返回的是在调用set_new_handler之前调用的new_handler函数(也就是在new_handler被替换之前的函数)。

你可以像下面这样使用set_new_handler:

1
2
3
4
5
6
7
8
9
10
11
12
13
// function to call if operator new can’t allocate enough memory
void outOfMem()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
std::set_new_handler(outOfMem);
int *pBigDataArray = new int[100000000L];
...
}

如果operaotr new无法为100,000,000个整数分配内存,就会调用outOfMem,也就是输出一个error信息之后程序终止(abort)。(顺便说一下,考虑在向cerr中写入error信息期间如果必须动态的分配内存会发生什么。。)

1.2 如何设计一个良好的new-handler函数

当operator new不能满足一个内存请求的时候,它会反复调用new-handler函数直到它发现有足够的内存可以分配了。引起这些函数被反复调用的代码在Item 51中可以找到,但是这种高级别的描述信息足够让我们得出结论:一个设计良好的new-handler函数必须能够做到如下几点。

  • 提供更多的可被使用的内存。这可以保证下次在operator new内部尝试分配内存时能够成功。实现这个策略的一种方法是在程序的开始阶段分配一大块内存,然后在第一次调用new-handler的时候释放它。
  • 安装一个不同的new-handler。如果当前的new-handler不能够为你提供更多的内存,可能另外一个new-handler可以。如果是这样,可以在当前的new-handler的位置上安装另外一个new-handler(通过调用set_new_handler)。下次operator new调用new-handler函数的时候,它会调用最近安装的。(这个主题的一个变种是一个使用new_handler来修改它自己的行为,所以在下次触发这个函数的时候,它就会做一些不同的事情。达到这个目的的一个方法是让new_handler修改影响new-handler行为的static数据,命名空间数据或者全局数据。)
  • 卸载new-handler,也就是为set_new_handler传递null指针。如果没有安装new-handler,operator new在内存分配失败的时候会抛出异常。
  • 没有返回值,调用abort或者exit。

这些选择让你在实现new-handler的时候有相当大的灵活性。

2. 为特定类指定new-handler

有时候你想用不同方式来处理内存分配失败,这依赖于需要分配内存的对象所属的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class X {
public:
static void outOfMemory();
...
};
class Y {
public:
static void outOfMemory();
...
};
X* p1 = new X; // if allocation is unsuccessful,
// call X::outOfMemory
Y* p2 = new Y; // if allocation is unsuccessful,
// call Y::outOfMemory

C++没有为类提供指定的new-handlers,但也不需要。你可以自己实现这种行为。你可以使每个类提供自己版本的set_new_handler和operator new。类中的set_new_handler允许客户为类提供new_handler(就像标准的set_new_handler允许客户指定全局的new-handler一样)。类的operator new确保为类对象分配内存时,会使用其指定的new-handler来替代全局new-handler。

2.1 在类中声明static new_handler成员

假设你想对Widget类对象的内存分配失败做一下处理。当operator new不能为Widget对象分配足够的内存的时候你必须跟踪一下函数调用过程,所以你要声明一个类型为new_handler的static成员,来指向这个类的new-handler函数。Widget将会是下面这个样子:

1
2
3
4
5
6
7
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 currentHandler;
};

静态类成员必须在类外部定义(除非他们是const整型,见Item 2),所以:

1
2
std::new_handler Widget::currentHandler = 0; // init to null in the class
// impl. File

Widget中的set_new_handler函数会把传递进去的指针(所指向的new-handler函数)保存起来,并且会返回调用set_new_handler之前所保存的指针。这也是标准版本set_new_handler的做法:

1
2
3
4
5
6
std::new_handler Widget::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}

2.2 重新定义operator new

最后,Widget的operator new将会做下面的事情:

  1. 调用标准set_new_handler,参数为Widget的错误处理函数。这就将Widget的new-handler安装成为了全局的new-handler。
  2. 调用全局的operator new来执行实际的内存分配。如果分配失败,全局的operator new会触发Widget的new-handler,因为这个函数已经被安装为全局new-handler。如果全局的operator new最终不能分配内存,它会抛出bad_alloc异常。在这种情况下,Widget的operator new必须恢复原来的全局new-handler,然后传播异常。为了确保源new-handler总是能被恢复,Widget将全局new-handler作为资源来处理,遵循Item 13的建议,使用资源管理对象来防止资源泄漏。
  3. 如果全局operator new能够为Widget对象分配足够的内存。Widget的operator new就会返回指向被分配内存的指针。管理全局new-handler的对象的析构函数会自动恢复调用Widget的operator new之前的new-handler。

这里我们以资源处理(resource-handling)类开始,只包含基本的RAII处理操作,包括在构造时获取资源和在在析构时释放资源(Item 13):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler nh) // acquire current
: handler(nh) {} // new-handler
~NewHandlerHolder() // release it
{ std::set_new_handler(handler); }
private:
std::new_handler handler; // remember it
NewHandlerHolder(const NewHandlerHolder&); // prevent copying
NewHandlerHolder& // ([see Item 14](http://www.cnblogs.com/harlanc/p/6414789.html "Item 14"))
operator=(const NewHandlerHolder&);
};

这会使得Widget的operator new的实现非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
NewHandlerHolder // install Widget’s
h(std::set_new_handler(currentHandler)); // new-handler
return ::operator new(size); // allocate memory
// or throw
} // restore global
// new-handler
void outOfMem(); // decl. of func. to call if mem. alloc.
// for Widget objects fails
Widget::set_new_handler(outOfMem); // set outOfMem as Widget’s
// new-handling function
Widget *pw1 = new Widget; // if memory allocation
// fails, call outOfMem
std::string *ps = new std::string; // if memory allocation fails,
// call the global new-handling
// function (if there is one)
Widget::set_new_handler(0); // set the Widget-specific
// new-handling function to
// nothing (i.e., null)
Widget *pw2 = new Widget; // if mem. alloc. fails, throw an
// exception immediately. (There is
// no new- handling function for
// class Widget.)

2.3 将NewHandlerHolder转换为模板

不管在什么类中,实现的这个主题的代码都是一样的,所以我们可以为其设一个合理的目标,就是代码能够在其他地方重用。达到这个目标的一个简单方法是创建一个“混合风格(mixin-style)”的基类,也就是设计一个基类,允许派生类继承单一特定的能力——在这个例子中,这种能力就是为类指定new-handler。然后将基类变为一个模板,于是你可以为每个继承类获得一份不同的类数据的拷贝。

这个设计的基类部分使得派生类能够继承它们都需要的**set_new_handler和operator new函数,同时设计的模板部分确保每个继承类获得一个不同的**currentHandler数据成员。说起来有些复杂,但是代码看上去很熟悉。事实上,唯一真正不一样的是现在任何类都能够获得这个功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
template<typename T> // “mixin-style” base class for
class NewHandlerSupport { // class-specific set_new_handler
public: // support
static std::new_handler set_new_handler(std::new_handler p) throw();
static void* operator new(std::size_t size) throw(std::bad_alloc);
... // other versions of op. new —
// see Item 52
private:
static std::new_handler currentHandler;
};
template<typename T>
std::new_handler
NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
{
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);
}
// this initializes each currentHandler to null
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;

有了这个类模板之后,向Widget中添加set_new_handler支持就变得容易了:Widget只需要继承自NewHandlerSupport。(这可能看上去比较独特,接下来我会进行详细的解释。)

1
2
3
4
class Widget: public NewHandlerSupport<Widget> {
... // as before, but without declarations for
}; // set_new_handler or operator new

这是Widget提供一个特定的set_new_handler需要做的所有事情。

但是对于Widget继承自NewHandlerSupport,你可能还是有些不安。如果是这样,当你注意到NewHandlerSupport模板永远不会使用类型参数T之后你的不安可能会加剧。你没有必要这样。对于每个继承自NewHandlerSupport的类来说,我们所有需要的是一份不同的NewHandlerSupport的拷贝——特别是静态数据成员currentHandler的不同拷贝。模板机制自身会为每个T自动生成currentHandler的一份拷贝,NewHandlerSupport使用这个T来进行实例化。

对于Widget继承自一个使用Widget作为类型参数的模板基类来说,如果这个概念让你感觉眩晕,不要感觉不好。每个人看到开始看到它的时候都会有这种感觉。但是,它是非常有用的技术,它有一个名字,这个名字如果这个概念一样,第一次看到它的人没有人会感觉它很自然,它叫做怪异的循环模板模式(curiously
recurring template pattern CRTP)。

我曾经写过一遍文章建议为它起一个更好的名字:do it for me,因为当Widget继承自NewHandlerSupport,它真的像是在说:“我是Widget,我需要为Widget继承NewHandlerSupport类“。没有人使用我建议的名字,但是使用“do it for me”来想象一下CRTP可能会帮助你理解模板化的继承会做什么。

有了像NewHandlerSupport这样的模板,为任何需要new-hadler的类添加一个特定的new-handler就会变得容易。混合风格的继承总是会将你引入多继承的主题,在开始进入这个主题之前,你可能想读一下Item
40

3. Nothrow版本的new

直到1993年,当不能满足分配内存的要求时,C++要求operator new要返回null。现在指定operator new要抛出bad_alloc异常,但是大量的C++是在编译器支持修订版本之前写出来的。C++标准委员会也不想废弃test-for-null的代码,所以它们为operator
new提供了一种替代形式,它能够提供传统的“失败产生null(failure-yields-null)”行为。这些形式被叫做“nothrow”形式,某种程度上是因为他们使用了不会抛出异常的对象(定义在头文件中),new在这种情况下被使用:

1
2
3
4
5
6
7
8
9
10
class Widget { ... };
Widget *pw1 = new Widget; // throws bad_alloc if
// allocation fails
if (pw1 == 0) ... // this test must fail
Widget *pw2 = new (std::nothrow) Widget; // returns 0 if allocation for
// the Widget fails
if (pw2 == 0) ... // this test may succeed

nothrow版本的new不会像从表面上看起来这样可靠,对于异常它没有提供让人信服的保证。对于表达式“new (std::nothrow) Widget”,会发生两件事情。首先,通过调用nothrow版本的operator
new来为一个Widget 对象分配足够的内存。如果分配失败了,operator
new会返回null指针。然而如果分配成功了,Widget构造函数会被调用,到这个时候,就会世事难料了。Widget构造函数能够做任何它想做的。它自己可能new一些内存,如果是这样,并没有强迫它使用nothrow版本的new。虽然在”new (std::nothrow) Widget”中的operator
new不会抛出异常,但是Widget构造函数却可能抛出来。如果是这样,异常会像平时一样传播出去。结论是什么?使用nothrow
new只能保证operator new不会抛出异常,不能保证像“new(std::nothrow)
Widget”这样的表达式不抛出异常。十有八九,你将永远不会有使用nothrow new的需要。

不论你是使用”普通的”(也就是抛出异常的)new还是nothrow版本的new,重要的是你需要明白new-handler的行为,因为在两种new中都会使用到它。

4. 总结

  • Set_new_handler允许你在分配内存不能满足要求的时候指定一个特定的被调用的函数。
  • Nothrow new功能有限,因为它只能被应用在内存分配上;相关联的构造函数调用可能仍然会抛出异常。