在C++语言中,资源管理(Managing Resources)始终是一个十分重要的话题,也是程序员在使用C++语言编写代码时需要十分注意的地方,稍有不慎就可能导致资源泄漏(resource leak),在笔者以往的编程实践中就经常遇到此类问题。而“resource acquisition in initialization”是一种处理此类问题的较好方法,这是Stroustrup博士在演讲中所提到的。关于这一点,在博士所著的D&E [1] 以及相关论文 [2] 中也有所提及。该方法使用一个类来代表对资源的管理逻辑,将指向资源的句柄(指针或引用)通过构造函数传递给该类,在该类的实例被销毁时由析构函数负责释放资源。可以在创建该类的实例之前申请资源,也可以在构造时由该类负责申请资源。这种方式的基本思路是,不论异常是否发生,由于C++的语言机制保证了,一定会调用位于当前范围(scope)的对象的析构函数,所以只要在析构函数中加入资源回收的代码,那么这些代码总是会被执行的。这种方法的好处在于,由于将资源回收的逻辑通过单独的类从原有代码中剥离出来,使程序员总是不会遗漏,思路也变得清晰。
以笔者之见,“resource acquisition in initialization”技法,在处理有关异常的问题时,其适用范围还可以扩展。不单涉及资源管理,只要当scope里存在类似于fopen/fclose、new/delete这样的对称操作时,就可以酌情考虑采用这种方法。避免资源泄漏固然是头等大事,应该列于基本保证(basic guarantee)之内。但某些对称操作,如果会影响程序的正常执行甚至是产生致命错误(fatal error)的话,那么也是不可轻视的。而对于一个软件而言,杜绝fatal error应该也算是一个basic guarantee了。
以下是笔者在实践中遇到的一个例子。有意思的是,这个例子是本人在所负责的软件模块中首次决定使用异常处理机制所遇到的,可谓出师不利:)经过简化后的代码基本如下:
void f(C *pObj)
{
pObj->Editable(true);
// do some work with object
pObj->Editable(false);
}
函数f的作用是对传入其scope的pObj所指对象进行某些操作。当最初引入异常处理机制时,代码改变如下:
void f(C *pObj)
{
pObj->Editable(true);
try {
// do some work with object
// may cause exception
} catch(...)
{
// do some thing and rethrow
throw;
}
pObj->Editable(false);
}
在找到了错误根源之后,笔者采用了如下的补救措施,这一做法被Stroustrup博士称为naive use:
void f(C *pObj)
{
pObj->Editable(true);
try {
// do some work with object
// may cause exception
} catch(...)
{
// do some thing and rethrow
pObj->Editable(false);
throw;
}
pObj->Editable(false);
}
在写下这段代码的时候,直觉告诉自己,这里存在Bed Smell,但是由于时间紧迫,所以当时暂且容忍了这种Quick and Dirty的做法。正如Stroustrup博士在D&E中所指出的,这种做法的缺点是啰嗦,冗长乏味,而且可能代价昂贵。仔细分析一下,就可以看出这里存在的潜在危险:两处pObj->Editable(false)事实上是重复代码,我们需要始终保持两处代码的一致性,如果一段时间后,需要在pObj中增加一种类似Editable的属性,这种一致性的保持,就需要延续,很难保证不会再次疏忽。
于是,遵照大师的教诲,笔者增加了一个辅助类,代码如下:
class C_Handle {
C* _pObj;
public:
C_Handle(C* pObj) {
_pObj = pObj;
_pObj->Editable(true);
// may be other operations
}
~C_Handle() {
_pObj->Editable(false);
// also may be operations according to ctor
}
operator C* () { return _pObj; }
};
C_Handle的构造函数和析构函数中,对_pObj所指对象的操作是成对出现的,所以在以后扩展时也不容易出错。此时f函数的代码也变得简洁了许多:
void f(C* pObj)
{
C_Handle ch(pObj);
try {
// do some work with object
// may cause exception
} catch(...)
{
// do some thing and rethrow
throw;
}
}
个人觉得,这种技法应该具有普遍意义。现总结如下:在某个scope内出现针对某个对象的若干对称操作,而在彼此对称的两组操作间可能抛出异常以破坏这种对称性,并且这种破坏将导致与该scope相关的某种断言为假时,就可以考虑使用类似于Stroustrup博士在处理资源管理问题时所推荐的这种“resource acquisition in initialization”技法。甚至可以认为,资源管理中发生的例子是这里所提到的情形的一个特例。在资源管理方面的另一个很典型的例子是智能指针(Smart Pointer)[3]。
[1] B. Stroustrup,The Design and Evolution of C++ ,Addison-Wesley ,1994
[2] B. Stroustrup,Programming with Exceptions,InformIt.com,April 2001
[3] Scott Mayers,More Effective C++,Addison-Wesley,1995
[4] Andrei Alexandrescu and Petru Marginean,Generic<Programming>: Simplify Your Exception-Safe Code(http://www.cuj.com/experts/1812/alexandr.htm?topic=experts)