在C++的使用当中,最令人头疼的地方莫非是内存管理或者异常的使用。
想写出一个真正异常安全的代码是非常难得,需要考虑的因素有非常多。
在现代C++当中也有很多人提倡不使用异常,但是要完全杜绝使用C++异常
也是很难的,除非打算不使用任何一个标准库,重写所有需要用的数据结构算法等等。
在一般情况下,适当的使用标准库也能提高程序的开发效率,和健壮性。
毕竟标准库已经被广泛使用,代码质量高,但是使用异常会导致代码的膨胀。
为此,我们只能保证最少的使用异常,但是编写异常安全的函数是必不可少的。
void MyClass::foo() { lock(&mutex); delete buffer; ++counter; buffer = new CBuffer(); unlock(&mutex); }
从“异常安全性”的观点来看,这个函数很糟。“异常安全”有两个条件,而这个函数没有满足其中的任何一个条件
当异常被抛出时,带有异常安全性的函数会:
- 不泄露任何资源。上述代码没有做到,因为一旦new CBuffer()导致异常,unlock就永远不会调用,于是就产生了死锁。
- 不允许数据破坏。如果new CBuffer()导致异常,buffer就指向了一个已经被删除的对象,counter也已经被累加。
使用RAII保证在出现异常时正确的释放资源
void MyClass::foo() { Lock lock(&mutex); delete buffer; ++counter; buffer = new CBuffer(); }
目前死锁的问题被解决了,但是数据破坏的问题还没有被解决。
此刻我们需要做一些抉择,在抉择之前我们必须先面对一些用来定义选项的术语。
异常安全函数提供一下三个保证之一:
- 基本承诺:如果异常被抛出,程序内的任何事物仍然保证在有效状态下。
- 强烈保证:如果异常被抛出,程序状态不改变,拥有原子性,要么全部成功,要么回到调用函数前的状态。
- 不抛掷(nothrow)保证:承诺绝对不抛出异常。
我们可以采用一种策略。这个策略被称为:copy and swap。
原则很简单,打算为所有要改变的对象做出一份副本,然后在副本上面进行修改,
待所有副本修改完毕,再将副本与原对象在一个不抛出异常的操作中置换出来(swap)。
struct PMImpl { std::shared_ptr<CBuffer> buffer; int counter; }; void MyClass::foo() { Lock lock(&mutex); // 使用基于RAII的指针管理方式 std::shared_ptr<PMImpl> new_impl(new PMImpl ); new_impl->buffer.reset(new CBuffer); new_impl->counter = pimpl->counter + 1; // 使用不抛出异常的swap swap(pimpl, new_impl); }
“copy and swap”策略是对对象状态做出”全有或者全无“改变的一个很好的办法。
一个软件系统要么就具备异常安全性,要不就全然否定,没有所谓的”局部异常安全系统“。
如果系统内有一个函数不具备异常安全性,整个系统就不具备异常安全性。
四十年前,满载goto的代码被视为一种美好实践,而今我们却致力写出结构化控制流。二十年前,全局数据被视为一种美好实践,而今我们却致力于数据的封装。十年前,写“未将异常考虑在内”的函数被视为一种美好实践,而今我们致力写出“异常安全码”。时间不断前进,我们与时俱进。