异常安全与异常独立

《Exceptional C++》里涉及到两个概念,异常安全和异常独立。

异常安全是指在发生异常的情况下能够捕捉并处理异常,并且异常前后对象或上下文状态一致、正确。
异常独立是指允许有发生异常的可能,但自身不处理,将这个异常视为不可忽视的问题从而传递给调用方。
这里从内存(或资源)分配的角度来看什么样的代码至少是异常安全和异常独立的。
假设某个容器类的一个拷贝函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<class T>
T* NewCopy( const T* src, size_t srcsize, size_t destsize)
{
assert( destsize >= srcsize );
T* dest = new T[destsize]; // 异常独立
try{
copy(src, src+srcsize, dest); // stl 算法
}
catch(...)
{
delete [] dest; // 异常安全处理
throw; // 重新抛出,异常独立
}
return dest;
}

之所以new T[destsize]语句没有放在try块中处理是因为如果new []()动作失败,内存还没有分配,所以不存在内存泄露,直接向上抛出bad_alloc异常。但是如果T的构造函数发生异常,所有已构造的T会被自动销毁,delete[ ]()也会被调用。因此new这句属于异常独立。直到new语句完全执行完毕后,才担负起释放内存的责任,因此,如果此时发生异常,为了异常安全必须在捕捉到异常后释放内存或资源,使其回到函数调用前的正确状态。然后转为异常独立的考虑。假如T的operator=发生异常,导致copy无法执行,那这个无法避免的异常导致了非预期的结果,就必须再重新抛出异常。

换成auto_ptr或者unique_ptr之类的智能指针那问题就轻松多了,连try块都可以去掉。但是举这个例子的目的是总结异常安全和异常独立的两种角度考虑。如果说例子中不是数据拷贝,而是将数据写入磁盘文件,那么就有同样的考虑。

个人认为:
对资源分配前(or 正在分配)或者事务开始前的预处理动作中遇到的异常应该按照异常独立来处理,因为通常此类异常是无法避免的、外部导致的严重异常。应当不处理,选择直接自动抛出。

对资源已经分配或者事务已经开始处理(开始写入文件 or 数据已被修改)到最后完成状态中发生的异常必须要按照异常安全来处理,使其回到正确的状态(用数据库里的术语叫回滚,rollback)。如果是可以Hold住的可预见异常,那么程序可以继续执行;如果是不可预见的异常(上例中的模板参数类型T),那必须重新抛出。