读书笔记 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>中:
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:
// 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
有时候你想用不同方式来处理内存分配失败,这依赖于需要分配内存的对象所属的类:
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将会是下面这个样子:
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),所以:
std::new_handler Widget::currentHandler = ; // init to null in the class
// impl. File
Widget中的set_new_handler函数会把传递进去的指针(所指向的new-handler函数)保存起来,并且会返回调用set_new_handler之前所保存的指针。这也是标准版本set_new_handler的做法:
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将会做下面的事情:
- 调用标准set_new_handler,参数为Widget的错误处理函数。这就将Widget的new-handler安装成为了全局的new-handler。
- 调用全局的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的建议,使用资源管理对象来防止资源泄漏。
- 如果全局operator new能够为Widget对象分配足够的内存。Widget的operator new就会返回指向被分配内存的指针。管理全局new-handler的对象的析构函数会自动恢复调用Widget的operator new之前的new-handler。
这里我们以资源处理(resource-handling)类开始,只包含基本的RAII处理操作,包括在构造时获取资源和在在析构时释放资源(Item 13):
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) operator=(const NewHandlerHolder&); };
这会使得Widget的operator new的实现非常简单:
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(); // 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数据成员。说起来有些复杂,但是代码看上去很熟悉。事实上,唯一真正不一样的是现在任何类都能够获得这个功能:
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 = ;
有了这个类模板之后,向Widget中添加set_new_handler支持就变得容易了:Widget只需要继承自NewHandlerSupport<Widget>。(这可能看上去比较独特,接下来我会进行详细的解释。)
class Widget: public NewHandlerSupport<Widget> {
... // as before, but without declarations for }; // 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在这种情况下被使用:
class Widget { ... };
Widget *pw1 = new Widget; // throws bad_alloc if
// allocation fails if (pw1 == ) ... // this test must fail Widget *pw2 = new (std::nothrow) Widget; // returns 0 if allocation for
// the Widget fails if (pw2 == ) ... // 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功能有限,因为它只能被应用在内存分配上;相关联的构造函数调用可能仍然会抛出异常。
读书笔记 effective c++ Item 49 理解new-handler的行为的更多相关文章
- 读书笔记 effective c++ Item 30 理解内联的里里外外 (大师入场啦)
最近北京房价蹭蹭猛涨,买了房子的人心花怒放,没买的人心惊肉跳,咬牙切齿,楼主作为北漂无房一族,着实又亚历山大了一把,这些天晚上睡觉总是很难入睡,即使入睡,也是浮梦连篇,即使亚历山大,对C++的热情和追 ...
- 读书笔记 effective c++ Item 41 理解隐式接口和编译期多态
1. 显示接口和运行时多态 面向对象编程的世界围绕着显式接口和运行时多态.举个例子,考虑下面的类(无意义的类), class Widget { public: Widget(); virtual ~W ...
- 读书笔记 effective c++ Item 42 理解typename的两种意义
1. class和typename意义相同的例子 问题:在下面的模板声明中class和typename的区别是什么? template<class T> class Widget; // ...
- 读书笔记 effective c++ Item 42 理解typename的两种涵义
1. class和typename含义相同的例子 问题:在下面的模板声明中class和typename的区别是什么? template<class T> class Widget; // ...
- 读书笔记 effective c++ Item 29 为异常安全的代码而努力
异常安全在某种意义上来说就像怀孕...但是稍微想一想.在没有求婚之前我们不能真正的讨论生殖问题. 假设我们有一个表示GUI菜单的类,这个GUI菜单有背景图片.这个类将被使用在多线程环境中,所以需要mu ...
- 读书笔记 effective c++ Item 16 成对使用new和delete时要用相同的形式
1. 一个错误释放内存的例子 下面的场景会有什么错? std::]; ... delete stringArray 一切看上去都是有序的.new匹配了一个delete.但有一些地方确实是错了.程序的行 ...
- 读书笔记 effective c++ Item 51 实现new和delete的时候要遵守约定
Item 50中解释了在什么情况下你可能想实现自己版本的operator new和operator delete,但是没有解释当你实现的时候需要遵守的约定.遵守这些规则并不是很困难,但是它们其中有一些 ...
- 读书笔记 effective c++ Item 1 将c++视为一个语言联邦
Item 1 将c++视为一个语言联邦 如今的c++已经是一个多重泛型变成语言.支持过程化,面向对象,函数式,泛型和元编程的组合.这种强大使得c++无可匹敌,却也带来了一些问题.所有“合适的”规则看上 ...
- 读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数
关于构造函数的一个违反直觉的行为 我会以重复标题开始:你不应该在构造或者析构的过程中调用虚函数,因为这些调用的结果会和你想的不一样.如果你同时是一个java或者c#程序员,那么请着重注意这个条款,因为 ...
随机推荐
- sql server中部分函数功能详解
1.TOP 子句 TOP 子句用于规定要返回的记录的数目. 对于拥有数千条记录的大型表来说,TOP 子句是非常有用的. SQL Server 的语法: SELECT TOP number|percen ...
- Struts2之访问路径
上一篇已经和大家分享了关于Struts2命名空间和Action的三种创建方式,本篇我们接着命名空间的内容,来一起探讨一下关于Struts2的访问路径问题,何为访问路径,就是指当我们在浏览器输入地址,点 ...
- 面向对象 "一"
1:面向对象不是所有情况都适用. 2面向对象编程 a:定义类 calss Foo: 注意顶一个类的时候首字母必须是大写 def (方法一)(self,bb) pass b:根据创建对象,创建和Foo实 ...
- react 组件的生命周期
组件的生命周期 过程 装载(Mounting) :组件被插入到 DOM 中: 更新(Updating) :组件重新渲染以更新 DOM: 卸载(Unmounting) :组件从 DOM 中移除. 过程 ...
- ReactiveSwift框架
最近项目不多,所以就研究了一下RxSwift和RAS,RAC以前项目中用过了,在这里我就先简单的介绍一下什么是RAS.总述:在RAC 5.0这个版本,有了很大的改动,API已经重新命名.在和Swift ...
- 为什么每个浏览器User-Agent都是Mozilla?真相原来是这样!
转载自简明现代魔法http://www.nowamagic.net/librarys/veda/detail/2576 故事还得从头说起,最初的主角叫NCSA Mosaic,简称Mosaic(马赛克) ...
- Maven settings.xml配置解读
本文对${maven.home}\conf\settings.xml的官方文档作个简单的解读,请确保自己的maven环境安装成功,具体安装流程详见Maven安装 第一步:看settings.xml的内 ...
- DotNet友元程序集解析
项目开发的过程中,调试使用的可能是最多的操作.任何代码写出来都需要经过调试和整合,以此扩展和提升程序的稳定性和可靠性.谈到.NET的单元测试,在这里就得提提.NET的友元程序集这一特性,也借用.NET ...
- PHP-配置方法
由于php是一个zip文件(非install版),安装较为简单,解压就行.把解压的 php5.2.1-Win32重命名为 php5.并复制到C盘目录下.即安装路径为 c:\php5 1 找到php目录 ...
- iOS UI控件总结(全)
1.UIButton UIButton *btn = [UIButton buttonWithType:UIButtonTypeRoundedRect]; btn.frame = CGRectMake ...