用ScopeGuard简化异常安全代码

ScopeGuard是Loki库里的一个组件,用来在c++中进行局部资源管理。

很多时候,在一个函数内部会有资源的申请和释放,如果在函数内部出现的异常,那么要完全释放资源是比较痛苦的,常见的作法是对所有可能产生异常的地 方try...catch,不管是否有异常,申请的资源都是要释放的。但是这样的写法造成代码体积很大,而且很不易读。解决的方法是使用c++的 RAII(资源申请即初始化),可以对每个资源的申请和释放封装一个类,在类的构造函数中申请,析构函数中释放,这样只要在使用的函数内部生成这个对象的 一个实例,不论是否产生异常,编译器都会调用析构函数释放资源了。

不过这个方法不好的地方是,会产生一大堆很小的类,看起来肯定是不舒服,用起来也很不方便。

ScopeGuard用了一组模板类来代替对每个资源的申请/释放的一大堆小类。ScopeGuard实现的机制是,保存一个用于释放资源的函数在 ScopeGuard内部,当ScopeGuard析构(也就是作用域结束)时,在ScopeGuard的析构函数中调用保存的函数实现资源释放。如果在 析构之前调用了ScopeGuard对象的Dismiss()方法,那SocpeGuard就不再执行保存的函数。

ScopeGuard支持两种方式,一种是保存一个函数指针,另一种是保存了类成员函数。这在ScopeGuard内部分别通过 ScopeGuardImp和ObjScopeGuardImp来实现。它们都有一个共同的基类ScopeGuardImpBase,基类集中了对 dismissed_标志的管理。在ScopeGuard的析构函数中,会通过判断dismissed_标志来决定是否调用保存的函数。

在Loki库中,实现了6个ScopeGuardImp类,分别是ScopeGuardImp0, ScopeGuardImp1, ScopeGuardImp2, ScopeGuardImp3, ScopeGuardImp4, ScopeGuardImp5,它们的区别在于析构时要执行的函数参数个数不一样,ScopeGuardImp0用于没有参数的情 况,ScopeGuardImpl用于一个参数的情况...ScopeGuardImp5用于5个参数的情况。一般也不会有这么多参数的函数,如果确实碰 到了,那就自己实现吧。

对于ObjScopeGuardImp一共实现了4个类,分别是ObjScopeGuardImp0, ObjScopeGuardImp1, ObjScopeGuardImp2, ObjScopeGuardImp3,它们的区别也是在参数的个数上。ObjScopeGuardImp和ScopeGuardImp的区别在 于,ObjScopeGuardImp用于保存了一对象以及一个类成员函数,在析构的时候,会通过这个对象调用它的成员函数。它使用了不太为人所知的语 法:指向成员函数的指针和operator.*()。

直接使用上面这些类是很麻烦的,你必须自己记住有几个参数,各个参数的类型,然后生成这个类的实例。幸亏Loki提供了两个辅助函 数:MakeGuard和MakeObjGuard,故名思义,前一个用于生成ScopeGuardImp对象,后一个用于生成 ObjScopeGuardImp对象。MakeGuard和MakeObjGuard依靠编译器自动推导函数的参数,不再需要显式地创建对象。这个技巧 也被用于标准库中,如make_pair和bind1st。

MakeGuard和MakeObjGuard通过一个临时对象的引用的技巧来简体操作。根据C++标准,如果const的引用被初始化为对一个临 时变量的引用,那么它会使这个临时变量的生命期变得和它自己一样。这在"关于临时对象的引用"一文中有过描述。ScopeGuard被定义为 typedef const ScopeGuardImplBase& ScopeGuard,MakeGuard和MakeObjGuard虽然生成不同的ScopeGuardImpBase子类对象,但是都被赋给了 const ScopeGuardImplBase&这个基类对象的引用,这个临时对象的生命周期就会变成和这个基类引用的生命周期一致。这样就不用在 MakeGuard和MakeObjGuard的返回值的地方手写类型。

也就是说,MakeGuard和MakeObjGuard的返回值,必须要有一个值来存储,否则它返回的地方就析构了。

所以,ScopeGuard中定义了两个宏来简化返回值的操作,使用宏LOKI_ON_BLOCK_EXIT和LOKI_ON_BLOCK_EXIT_OBJ的话,会自动通过__LINE__生成一个不会相同的返回值。

你可能感兴趣的:(用ScopeGuard简化异常安全代码)