资源管理(Managing Resources)始终是C++语言一个十分重要的话题,也是程序员在使用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); } |
此处再度throw是为了使f的调用者能有机会做一些处理,这是在设计时所需要的。类似这样的做法在一般的异常处理程序中是很常见的,但是笔者的疏忽却另自己吃了大亏。虽然,从经过简化的代码中很容易看出破绽来,但是由于当时经验不足,加之程序逻辑复杂,直到测试时通过最终的用户界面才发现了问题。经过几个小时的艰苦调试,最后发现问题出在f函数。事实上,函数f的行为隐含了一个断言(assert),即:f保证不对pObj所指对象的不可编辑状态做出更改,在调用f前对象是不可编辑的,调用后仍然如此。而在上述程序中,当异常发生时,由于没有执行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); } |