保证异常安全
异常安全(exception safety)即保证不会因抛出异常而造成错误或资源泄漏等问题。
异常安全的级别:
- 基本保证(basic guarantee):抛出异常后,保证无资源泄漏,所有对象均处于有效状态
- 强保证(strong guarantee):如果某个过程抛出异常,保证程序的状态与执行该过程之前相同
- 不抛异常保证(nothrow guarantee):保证操作不会失败,也不会抛出任何异常
程序应至少提供基本保证,关键过程应实现强保证,与异常机制相关的过程,如析构函数、资源回收函数等,则应实现不抛异常保证。
示例:
void foo(vector<T>& v) {
v.push_back(T()); // Strong guarantee
}
例中标准容器 vector 的 push_back 函数提供强保证,如果抛出异常,容器中的元素以及容器的容量均不会发生任何变化。
又如:
void bar() {
lock();
procedure_may_throw(); // Non-compliant
unlock();
}
设 lock 是某种获取资源的操作,unlock 是释放资源的操作,procedure_may_throw 是可能抛出异常的过程,那么 bar 函数就不是异常安全的,一旦抛出异常就会导致死锁或泄漏等问题。
应保证资源从分配到回收的过程不被异常中断,如捕获异常,在重新抛出异常前释放资源:
void bar() {
lock();
try {
procedure_may_throw(); // Compliant, but verbose
} catch (...) {
unlock();
throw;
}
unlock();
}
但分散的回收操作容易出错,显式的 try-cath 语句也不利于维护。
将资源托管于类对象,使资源的生命周期协同于对象的生命周期,自动完成分配和回收是更好的方式:
void bar() {
LockOwner object; // Safe and brief
procedure_may_throw(); // Compliant
}
使 lock 和 unlock 分别由 object 的构造和析构函数完成,即使 procedure_may_throw 抛出异常,相关资源也会被自动回收,资源的对象化管理方法可参见 ID_ownerlessResource 的进一步讨论。
异常安全的另一个重要方面是抛出异常时应保证对象的状态是正确的,事务或算法在处理对象时可能要分多个步骤处理对象的多个成员,要注意中途抛出异常会造成数据不一致等问题。
class X {
T a, b;
public:
void job() {
proc(a); // May throw exceptions
proc(b); // Unsafe, ‘a’ and ‘b’ may be inconsistent
}
};
设 a 和 b 是两个密切相关的成员,如账号和金额等,job 是处理事务的函数,如果在中途抛出异常就会使对象处于错误的状态,解决方法可以考虑“复制 - 交换”模式,如:
class X {
T a, b;
public:
void job() {
X copy(*this);
proc(copy.a);
proc(copy.b);
swap(copy); // Copy-and-swap idiom
}
void swap(X&) noexcept; // Nothrow guarantee
};
先处理对象的副本,处理成功后交换副本与对象的数据,交换过程需要保证不抛出异常,这样从对象副本的生成到事务处理完毕的过程中即使抛出异常也不影响对象的状态,实现了强异常安全保证。