详解C++ new-handler机制
当operatornew不能满足一个内存分配请求时,它抛出一个exception(异常)。很久以前,他返回一个nullpointer(空指针),而一些比较老的编译器还在这样做。你依然能达到以前的目的(在一定程度上),但是我要到本文的最后再讨论它。
在operatornew因回应一个无法满足的内存请求而抛出一个exception之前,它先调用一个可以由客户指定的被称为new-handler的error-handlingfunction(错误处理函数)。(这并不完全确切,operatornew真正做的事情比这个稍微复杂一些,详细细节将在下一篇文章中讨论。)为了指定out-of-memory-handlingfunction,客户调用set_new_handler——一个在
namespacestd{ typedefvoid(*new_handler)(); new_handlerset_new_handler(new_handlerp)throw(); }
就像你能够看到的,new_handler是一个指针的typedef,这个指针指向不取得和返回任何东西的函数,而set_new_handler是一个取得和返回一个new_handler的函数。(set_new_handler的声明的结尾处的"throw()"是一个exceptionspecification(异常规范)。它基本上是说这个函数不会抛出任何异常,尽管真相更有趣一些。
set_new_handler的形参是一个指向函数的指针,这个函数是operatornew无法分配被请求的内存时应该调用的。set_new_handler的返回值是一个指向函数的指针,这个函数是set_new_handler被调用前有效的目标。
你可以像这样使用set_new_handler:
//functiontocallifoperatornewcan'tallocateenoughmemory voidoutOfMem() { std::cerr<<"Unabletosatisfyrequestformemory\n"; std::abort(); } intmain() { std::set_new_handler(outOfMem); int*pBigDataArray=newint[100000000L]; ... }
如果operatornew不能为100,000,000个整数分配空间,outOfMem将被调用,而程序将在发出一个错误信息后中止。(顺便说一句,考虑如果在写这个错误信息到cerr...的过程中内存必须被动态分配会发生什么。)
当operatornew不能满足一个内存请求时,它反复调用new-handlerfunction直到它能找到足够的内存。但是从这种高层次的描述已足够推导出一个设计得好的new-handlerfunction必须做到以下事情之一:
- ·Makemorememoryavailable(使得更多的内存可用)。这可能使得operatornew中下一次内存分配的尝试成功。实现这一策略的一个方法是在程序启动时分配一大块内存,然后在new-handler第一次被调用时释放它供程序使用。
- ·Installadifferentnew-handler(安装一个不同的new-handler)。如果当前的new-handler不能做到使更多的内存可用,或许它知道有一个不同的new-handler可以做到。如果是这样,当前的new-handler能在它自己的位置上安装另一个new-handler(通过调用set_new_handler)。operatornew下一次调用new-handlerfunction时,它会得到最近安装的那一个。(这个主线上的一个变化是让一个new-handler改变它自己的行为,这样,下一次它被调用时,可以做一些不同的事情。做到这一点的一个方法是让new-handler改变能影响new-handler行为的static(静态),namespace-specific(名字空间专用)或global(全局)的数据。)
- ·Deinstallthenew-handler(卸载new-handler),也就是,将空指针传给set_new_handler。没有new-handler被安装,当内存分配没有成功时,operatornew抛出一个异常。
- ·Throwanexception(抛出一个异常),类型为bad_alloc或继承自bad_alloc的其它类型。这样的异常不会被operatornew捕获,所以它们将被传播到发出内存请求的地方。
- ·Notreturn(不再返回),典型情况下,调用abort或exit。
这些选择使你在实现new-handlerfunctions时拥有极大的弹性。
有时你可能希望根据被分配object的不同,用不同的方法处理内存分配的失败:
classX{ public: staticvoidoutOfMemory(); ... }; classY{ public: staticvoidoutOfMemory(); ... }; X*p1=newX;//ifallocationisunsuccessful, //callX::outOfMemory Y*p2=newY;//ifallocationisunsuccessful, //callY::outOfMemory
C++没有对class-specificnew-handlers的支持,但是它也不需要。你可以自己实现这一行为。你只要让每一个class提供set_new_handler和operatornew的它自己的版本即可。class的set_new_handler允许客户为这个class指定new-handler(正像standardset_new_handler允许客户指定globalnew-handler)。class的operatornew确保当为classobjects分配内存时,class-specificnew-handler代替globalnew-handler被使用。
假设你要为Widgetclass处理内存分配失败。你就必须清楚当operatornew不能为一个Widgetobject分配足够的内存时所调用的函数,所以你需要声明一个new_handler类型的staticmember(静态成员)指向这个class的new-handlerfunction。Widget看起来就像这样:
classWidget{ public: staticstd::new_handlerset_new_handler(std::new_handlerp)throw(); staticvoid*operatornew(std::size_tsize)throw(std::bad_alloc); private: staticstd::new_handlercurrentHandler; };
staticclassmembers(静态类成员)必须在class定义外被定义(除非它们是const而且是integral),所以:
std::new_handlerWidget::currentHandler=0;//inittonullintheclass //impl.file
Widget中的set_new_handler函数会保存传递给它的任何指针,而且会返回前次调用时被保存的任何指针,这也正是set_new_handler的标准版本所做的事情:
std::new_handlerWidget::set_new_handler(std::new_handlerp)throw() { std::new_handleroldHandler=currentHandler; currentHandler=p; returnoldHandler; }
最终,Widget的operatornew将做下面这些事情:
以Widget的error-handlingfunction为参数调用standardset_new_handler。这样将Widget的new-handler安装为globalnew-handler。
调用globaloperatornew进行真正的内存分配。如果分配失败,globaloperatornew调用Widget的new-handler,因为那个函数刚才被安装为globalnew-handler。如果globaloperatornew最后还是无法分配内存,它会抛出一个bad_allocexception。在此情况下,Widget的operatornew必须恢复原来的globalnew-handler,然后传播那个exception。为了确保原来的new-handler总能被恢复,Widget将globalnew-handler作为一种资源对待,并遵循《C++箴言:使用对象管理资源》中的建议,使用resource-managingobjects(资源管理对象)来预防resourceleaks(资源泄漏)。
如果globaloperatornew能够为一个Widgetobject分配足够的内存,Widget的operatornew返回一个指向被分配内存的指针。object的用于管理globalnew-handler的destructor(析构函数)自动将globalnew-handler恢复到调用Widget的operatornew之前的状态。
以下就是你如何在C++中表达这所有的事情。我们以resource-handlingclass开始,组成部分中除了基本的RAII操作(在构造过程中获得资源并在析构过程中释放)(《C++箴言:使用对象管理资源》),没有更多的东西:
classNewHandlerHolder{ public: explicitNewHandlerHolder(std::new_handlernh)//acquirecurrent :handler(nh){}//new-handler ~NewHandlerHolder()//releaseit {std::set_new_handler(handler);} private: std::new_handlerhandler;//rememberit NewHandlerHolder(constNewHandlerHolder&);//preventcopying NewHandlerHolder&//(see《C++箴言:谨慎考虑资源管理类的拷贝行为》) operator=(constNewHandlerHolder&); };
这使得Widget的operatornew的实现非常简单:
void*Widget::operatornew(std::size_tsize)throw(std::bad_alloc) { NewHandlerHolder//installWidget's h(std::set_new_handler(currentHandler));//new-handler return::operatornew(size);//allocatememory //orthrow }//restoreglobal //new-handler
Widget的客户像这样使用它的new-handlingcapabilities(处理new的能力):
voidoutOfMem();//decl.offunc.tocallifmem.alloc. //forWidgetobjectsfails Widget::set_new_handler(outOfMem);//setoutOfMemasWidget's //new-handlingfunction Widget*pw1=newWidget;//ifmemoryallocation //fails,calloutOfMem std::string*ps=newstd::string;//ifmemoryallocationfails, //calltheglobalnew-handling //function(ifthereisone) Widget::set_new_handler(0);//settheWidget-specific //new-handlingfunctionto //nothing(i.e.,null) Widget*pw2=newWidget;//ifmem.alloc.fails,throwan //exceptionimmediately.(Thereis //nonew-handlingfunctionfor //classWidget.)
无论class是什么,实现这个方案的代码都是一样的,所以在其它地方重用它就是一个合理的目标。使它成为可能的一个简单方法是创建一个"mixin-style"baseclass(“混合风格”基类),也就是说,一个设计为允许derivedclasses(派生类)继承一个单一特定能力(在当前情况下,就是设定一个class-specificnew-handler的能力)的baseclass(基类)。然后把这个baseclass(基类)转化为一个template(模板),以便于你得到针对每一个inheritingclass(继承来的类)的classdata的不同拷贝。
这个设计的baseclass(基类)部分让derivedclasses(派生类)继承它们全都需要的set_new_handler和operatornewfunctions,而这个设计template(模板)部分确保每一个inheritingclass(继承来的类)得到一个不同的currentHandlerdatamember(数据成员)。这听起来可能有点复杂,但是代码看上去可靠而且熟悉。实际上,仅有的真正不同是它现在可以用在任何需要它的class之上:
template//"mixin-style"baseclassfor classNewHandlerSupport{ //class-specificset_new_handler public://support staticstd::new_handlerset_new_handler(std::new_handlerp)throw(); staticvoid*operatornew(std::size_tsize)throw(std::bad_alloc); ...//otherversionsofop.new private: staticstd::new_handlercurrentHandler; }; template std::new_handler NewHandlerSupport ::set_new_handler(std::new_handlerp)throw() { std::new_handleroldHandler=currentHandler; currentHandler=p; returnoldHandler; } template void*NewHandlerSupport ::operatornew(std::size_tsize) throw(std::bad_alloc) { NewHandlerHolderh(std::set_new_handler(currentHandler)); return::operatornew(size); } //thisinitializeseachcurrentHandlertonull template std::new_handlerNewHandlerSupport ::currentHandler=0;
有了这个classtemplate(类模板),为Widget增加set_new_handler支持就很容易了:Widget只需要从NewHandlerSupport
classWidget:publicNewHandlerSupport{ ...//asbefore,butwithoutdeclarationsfor };//set_new_handleroroperatornew
这些就是Widget为了提供一个class-specificset_new_handler所需要做的全部。
但是也许你依然在为Widget从NewHandlerSupport
对于Widget从一个把Widget当作一个typeparameter(类型参数)的templatizedbaseclass(模板化基类)继承,如果这个概念把你弄得有点糊涂,不必难受。它最开始对每一个人都有这种影响。然而,它发展成如此有用的一项技术,它有一个名字,虽然它正常看上去所反映的事实并不是他们第一次看到它的样子。它被称作curiouslyrecurringtemplatepattern(奇特的递归模板模式)(CRTP)。真的。
在这一点上,我发表了一篇文章建议一个更好的名字叫做"DoItForMe",因为当Widget从NewHandlerSupport
像NewHandlerSupport这样的templates使得为任何有需要的class添加一个class-specificnew-handler变得易如反掌。然而,mixin-styleinheritance(混合风格继承)总是会导致multipleinheritance(多继承)的话题,而在我们沿着这条路走下去之前,你需要阅读《C++箴言:谨慎使用多继承》。
直到1993年,C++还要求operatornew不能分配被请求的内存时要返回null。operatornew现在则被指定抛出一个bad_allocexception,但是很多C++程序是在编译器开始支持这个修订标准之前写成的。C++标准化委员会不想遗弃这些test-for-null(检验是否为null)的代码基础,所以他们提供了operatornew的另一种可选形式,用以提供传统的failure-yields-null(失败导致null)的行为。这些形式被称为"nothrow"形式,这在一定程度上是因为它们在使用new的地方使用了nothrowobjects(定义在头文件
classWidget{...}; Widget*pw1=newWidget;//throwsbad_allocif //allocationfails if(pw1==0)...//thistestmustfail Widget*pw2=new(std::nothrow)Widget;//returns0ifallocationfor //theWidgetfails if(pw2==0)...//thistestmaysucceed
对于异常,nothrownew提供了比最初看上去更少的强制保证。在表达式"new(std::nothrow)Widget"中,发生了两件事。首先,operatornew的nothrow版本被调用来为一个Widgetobject分配足够的内存。如果这个分配失败,众所周知,operatornew返回nullpointer。然而,如果它成功了,Widgetconstructor被调用,而在此刻,所有打的赌都失效了。Widgetconstructor能做任何它想做的事。它可能自己new出来一些内存,而如果它这样做了,它并没有被强迫使用nothrownew。那么,虽然在"new(std::nothrow)Widget"中调用的operatornew不会抛出,Widgetconstructor却可以。如果它这样做了,exception像往常一样被传播。结论?使用nothrownew只能保证operatornew不会抛出,不能保证一个像"new(std::nothrow)Widget"这样的表达式绝不会导致一个exception。在所有的可能性中,你最好绝不需要nothrownew。
无论你是使用"normal"(也就是说,exception-throwing)new,还是它的稍微有些矮小的堂兄弟,理解new-handler的行为是很重要的,因为它可以用于两种形式。
ThingstoRemember
- ·set_new_handler允许你指定一个当内存分配请求不能被满足时可以被调用的函数。
- ·nothrownew作用有限,因为它仅适用于内存分配,随后的constructor调用可能依然会抛出exceptions。
以上就是详解C++new-handler机制的详细内容,更多关于C++new-handler机制的资料请关注毛票票其它相关文章!