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

1. new-handler介绍

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

1.1 调用set_new_handler来指定全局new-handler

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

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

正如你所看到的,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 // function to call if operator new can’t allocate enough memory
 2 void outOfMem()
 3 {
 4 std::cerr << "Unable to satisfy request for memory\n";
 5 std::abort();
 6 }
 7
 8 int main()
 9 {
10 std::set_new_handler(outOfMem);
11 int *pBigDataArray = new int[100000000L];
12 ...
13 }

如果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 class X {
 2 public:
 3 static void outOfMemory();
 4 ...
 5 };
 6 class Y {
 7 public:
 8 static void outOfMemory();
 9 ...
10 };
11 X* p1 = new X; // if allocation is unsuccessful,
12 // call X::outOfMemory
13 Y* p2 = new Y; // if allocation is unsuccessful,
14 // 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 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 currentHandler;
7 };

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

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

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

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

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 class NewHandlerHolder {
 2 public:
 3 explicit NewHandlerHolder(std::new_handler nh) // acquire current
 4 : handler(nh) {} // new-handler
 5
 6 ~NewHandlerHolder()                             // release it
 7
 8 { std::set_new_handler(handler); }
 9
10 private:
11
12
13
14 std::new_handler handler;                             // remember it
15
16 NewHandlerHolder(const NewHandlerHolder&);      // prevent copying
17
18
19
20 NewHandlerHolder&                                   // (see Item 14)
21
22 operator=(const NewHandlerHolder&);
23
24 };         

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

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

2.3 将NewHandlerHolder转换为模板

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

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

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

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

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

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

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

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

我曾经写过一遍文章建议为它起一个更好的名字:do it for me,因为当Widget继承自NewHandlerSupport<Widget>,它真的像是在说:“我是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>中),new在这种情况下被使用:

 1 class Widget { ... };
 2 Widget *pw1 = new Widget;                        // throws bad_alloc if
 3 // allocation fails
 4
 5 if (pw1 == 0) ...                                             // this test must fail
 6
 7 Widget *pw2 = new (std::nothrow) Widget;   // returns 0 if allocation for
 8 // the Widget fails
 9
10 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功能有限,因为它只能被应用在内存分配上;相关联的构造函数调用可能仍然会抛出异常。
时间: 2024-10-07 05:51:05

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

读书笔记 effective c++ Item 30 理解内联的里里外外 (大师入场啦)

最近北京房价蹭蹭猛涨,买了房子的人心花怒放,没买的人心惊肉跳,咬牙切齿,楼主作为北漂无房一族,着实又亚历山大了一把,这些天晚上睡觉总是很难入睡,即使入睡,也是浮梦连篇,即使亚历山大,对C++的热情和追求还是不减,应该是感动了周公吧,梦境从此处开始,大师入场来给我安慰了... 11点躺在床上了,脑子里总结一下最近的工作:最近的开发用到inline函数比较多,众所周知,inline的使用是为了提高程序性能,可结果却总不尽如人意,这个捉急啊,嗯?怎么突然到了山脚下,周边树木林立,郁郁葱葱,鸟儿委婉啼叫

读书笔记 effective c++ Item 41 理解隐式接口和编译期多态

1. 显示接口和运行时多态 面向对象编程的世界围绕着显式接口和运行时多态.举个例子,考虑下面的类(无意义的类), 1 class Widget { 2 public: 3 Widget(); 4 virtual ~Widget(); 5 6 virtual std::size_t size() const; 7 virtual void normalize(); 8 9 void swap(Widget& other); // see Item 25 10 11 ... 12 13 }; 考虑下

读书笔记 effective c++ Item 51 实现new和delete的时候要遵守约定

Item 50中解释了在什么情况下你可能想实现自己版本的operator new和operator delete,但是没有解释当你实现的时候需要遵守的约定.遵守这些规则并不是很困难,但是它们其中有一些并不直观,所以知道这些规则是什么很重要. 1. 定义operator new的约定 1.1 约定列举 我们以operator new开始.实现一个一致的operator new需要有正确的返回值,在没有足够内存的时候调用new-handling函数(见Item 49),并且做好准备处理没有内存可分配

读书笔记 effective c++ Item 53 关注编译器发出的警告

许多程序员常常忽略编译器发出的警告.毕竟,如果问题很严重,它才将会变成一个error,不是么?相对来说,这个想法可能在其它语言是无害的,但是在C++中,我敢打赌编译器的实现者对于对接下来会发生什么比你有更好的理解.例如,下面的错误是每个人都时不时会犯的: 1 class B { 2 public: 3 virtual void f() const; 4 }; 5 class D: public B { 6 public: 7 virtual void f(); 8 }; 1. 你对警告的理解可能

读书笔记 effective c++ Item 35 考虑虚函数的替代者

1. 突破思维——不要将思维限定在面向对象方法上 你正在制作一个视频游戏,你正在为游戏中的人物设计一个类继承体系.你的游戏处在农耕时代,人类很容易受伤或者说健康度降低.因此你决定为其提供一个成员函数,healthValue,返回一个整型值来表明一个人物的健康度.因为不同的人物会用不同的方式来计算健康度,将healthValue声明为虚函数看上去是一个比较明显的设计方式: 1 class GameCharacter { 2 public: 3 4 virtual int healthValue()

读书笔记 effective c++ Item 54 让你自己熟悉包括TR1在内的标准库

1. C++0x的历史渊源 C++标准——也就是定义语言的文档和程序库——在1998被批准.在2003年,一个小的“修复bug”版本被发布.然而标准委员会仍然在继续他们的工作,一个“2.0版本”的C++标准预计在2009年被发布(虽然所有的工作很有可能在2007年底被完成).直到现在,发布下一版C++的预计年份还没有被确定,这就解释了为什么人们把下一版C++叫做“C++0x”——C++的200x年版本. C++0x可能会包含一些有趣的新的语言特性,但是大多数新C++功能将会以标准库附加物的形式被

读书笔记 effective c++ Item 52 如果你实现了placement new,你也要实现placement delete

1. 调用普通版本的operator new抛出异常会发生什么? Placement new和placement delete不是C++动物园中最常遇到的猛兽,所以你不用担心你对它们不熟悉.当你像下面这样实现一个new表达式的时候,回忆一下Item 16和Item 17: 1 Widget *pw = new Widget; 两个函数会被调用:一个是调用operator new来分配内存,第二个是Widget的默认构造函数. 假设第一个调用成功了,但是调用第二个函数抛出了异常.在这种情况下,对步

读书笔记 effective c++ Item 44 将与模板参数无关的代码抽离出来

1. 使用模板可能导致代码膨胀 使用模板是节省时间和避免代码重用的很好的方法.你不需要手动输入20个相同的类名,每个类有15个成员函数,相反,你只需要输入一个类模板,然后让编译器来为你实例化20个特定的类和300个你需要的函数.(只有在被使用的情况下类模版的成员函数才会被隐式的实例化,所以只有在300个函数被实际用到的情况下才会生成300个成员函数.)函数模板同样吸引人.你不用手动实现许多函数,你只需要实现一个函数模板,然后让编译器来做余下的事情. 然而在有些时候,如果你不小心,使用模板会导致代

读书笔记 effective c++ Item 32 确保public继承建立“is-a”模型

1. 何为public继承的”is-a”关系 在C++面向对象准则中最重要的准则是:public继承意味着“is-a”.记住这个准则. 如果你实现一个类D(derived)public继承自类B(base),你在告诉c++编译器(也在告诉代码阅读者),每个类型D的对象也是一个类型B的对象,反过来说是不对的.你正在诉说B比D表示了一个更为一般的概念,而D比B表现了一个更为特殊的概念.你在主张:任何可以使用类型B的地方,也能使用类型D,因为每个类型D的对象都是类型B的对象:反过来却不对,也就是可以使