C++:编写异常安全代码

在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的代码被视为一种美好实践,而今我们却致力写出结构化控制流。二十年前,全局数据被视为一种美好实践,而今我们却致力于数据的封装。十年前,写“未将异常考虑在内”的函数被视为一种美好实践,而今我们致力写出“异常安全码”。时间不断前进,我们与时俱进。

posted @ 2018-11-24 14:50  wind飘雪  阅读(900)  评论(0编辑  收藏  举报