《Effective C++》学习笔记(条款29:为“异常安全”而努力是值得的)

最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!

假如我们在给一个GUI写一个可更换背景的菜单类。这个类用于多线程环境,所以它有个互斥锁(mutex)作为并发控制之用:

class PrettyMenu{
public:
    ...
    void changeBackground(std::istream& imgSrc);
private:
    Mutex mutex;	//互斥锁
    Image* bgImg;  	//当前背景图片
    int imgChange; 	//背景被更改的次数
};

下面是 PrettyMenu 类更换背景图片的函数:

void PrettyMenu::changeBackground(std::istream& imgSrc){
    lock(&mutex);  				//上锁
    delete bgImg;  				//去掉老背景
    ++imgChange;   				//更新计数器
    bgImg = new Image(imgSrc); 	//换上新背景
    unlock(&mutex); 			//解锁
}

从“异常安全性”的角度来看,这个函数很危险。“异常安全”有两个条件,上述函数任何一个都没满足。

当异常被抛出时,带有“异常安全性”的函数会:

  • 不泄漏任何资源。一旦 new Image(imgSrc) 抛出异常,unlock(&mutex) 就不会调用,这个 mutex 就永远上锁了。
  • 不允许破坏数据。如果 new Image(imgSrc) 抛出异常,bgImg 就是指向一个被删除的对象,imgChange 也已被累加

解决资源泄漏的问题很容易,条款13讨论过如何以对象管理资源,而条款14也设计过 Lock 类作为一种确保互斥锁被及时释放的方法:

void PrettyMenu::changeBackground(std::istream& imgSrc){
    Lock ml(&mutex); //安全的Lock类详见第14章
    delete bgImg;
    ++imgChange;
    bgImg = new Image(imgSrc);
}

在解决数据被破坏的问题前,我们来了解下异常安全的三个级别:

  • 基本保证:如果抛出了异常,函数也能在有效的状态下运行,没有对象或数据损坏,所有对象也保持内部一致。然而程序本身则可能处于不确定状态。例如用户使用 changeBackground() 时抛出了异常,PrettyMenu 对象可能依然持有原来的背景,或者持有默认的背景,但具体哪个则是不确定的。
  • 强烈保证:如果抛出了异常,程序的状态不会改变。这就意味着对强保证函数的调用是原子性的(atomic),如果成功了就成功了,如果失败了就像什么都没有发生一样。强保证函数比基本保证函数更容易使用,因为强保证函数只能导致两种状态,成功或者不变,而基本保证的函数可能处于任何状态——只要那是个合法状态。
  • 不抛出保证:保证永远不会抛出异常。例如所有对于内置类型(int,指针等等)的操作都提供不抛出保证。它是异常安全代码中一个必不可少的关键基础材料。

如果我们假设,不给函数规定抛出什么异常,即写一个空的异常规范(empty exception specification),那它不就能提供最安全的不抛出保证了吗?

int doSomething() throw(); //空的异常规范

这并不是说 doSomething() 绝不会抛出异常,而是说如果它抛出异常,将是严重错误,然后触发 unexpected() 函数,而 unexpected() 默认会触发 terminate(),实际上doSomething() 也没有提供任何异常保证。异常安全取决于函数的实现,而不是函数的声明

异常安全的代码必须提供上述三种保证之一,不然它就不具备异常安全性。除非要用到异常不安全的老代码,否则一定要照顾到代码的异常安全性。当然,盲目追求最好也是不切实际的,不抛异常虽完美但显然很难做到。任何使用动态分配内存的东西(例如STL容器)都会在内存不足时抛出 bad_alloc 异常。所以可行的时候提供不抛出保证,大多数情况下还是要在基本和强保证中做出选择。

changeBackground() 而言,提供强烈保证几乎不难。首先把 PrettyMenubgImg 成员变量的类型从 Image* 改为用智能指针存储;然后重新排列changeBackground() 内的语句顺序,使得背景更换之后再累加 imgChange

class PrettyMenu{
    ...
  	shared_ptr<Image> bgImg; //使用shared_ptr
};
void PrettyMenu::changeBackground(std::istream& imgSrc){
      Lock ml(&mutex);
      bgImage.reset(new Image(imgSrc)); //替换新图片
      ++imgChange; 						//完成后再更新计数器
};

注意,这里不需要 delete 旧背景,因为智能指针已经帮你完成了。此外,删除动作只发生在新背景被成功创建之后,即 shared_ptr::reset() 只有在其参数(new Image(imgSrc)的执行结果)被成功生成后才会被调用,而 delete 在 reset() 函数内被执行。

前面说提供强烈保证几乎不难。如果 istream& 类型的它突然变得不可读了,Image 类的构造函数则可能会抛出异常,这种情况我们还没有照顾到,所以 changeBackground() 在解决这个问题之前只提供基本保证。

有个一般化的设计策略能够提供强烈保证,这个策略是先拷贝再交换(见条款11),原理很简单:拷贝你想要改变的对象,然后对拷贝对象执行操作,如果其中任何一步抛出异常,对象本体不会变。当成功执行完所有操作时,将拷贝对象和本体在一个不抛出异常的操作中交换。

实现上通常是将所有“属于对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象,这种手法称为 pimpl idiom(条款25有提到,条款31详细描述)。对 PrettyMenu 而言,典型写法如下:

struct PMImpl{ //PrettyMenuImplementation
    shared_ptr<Image> bgImage;
    int imgChange;
};

class PrettyMenu{
  ...
private:
    Mutex mutex;
    std::shared_ptr<PMImpl> pImpl;
};

void PrettyMenu::changeBackground(std::istream& imgSrc){
    using std::swap;   									//这句见条款25
    Lock ml(&mutex);   									//使用条款14安全管理mutex的Lock类,上锁
    std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); 	//生成数据的临时拷贝
    pNew->bgImg.reset(new Image(imgSrc)); 				//在拷贝上执行操作,用std::shared_ptr::reset替换图片
    ++pNew->imgChange;
    swap(pImpl,pNew); 									//调换回去
}//解锁

PMImpl 之所以使用 struct 而不是 class ,是因为 PrettyMenu 的数据封装性已经由 pImpl 处于 private 域而获得了保证。当然也可以用 class ,但有时候却不太方便。

copy and swap 策略是实现原子性操作(atomicity)——成功操作或者不做任何操作的一个好方法,但一般而言它并不保证整个函数有强烈的异常安全性。为了解原因,看下面的代码:

void someFunc(){
    ...//生成拷贝
    f1();
    f2();
    ...//把修改后的状态交换过来
}

如果 f1()f2() 的异常安全性比“强烈保证”低,就很难让 someFunc() 成为“强烈异常安全”。举个例子,假设 f1() 只提供基本保证,那么为了让 someFunc()提供强烈保证,我们必须获得调用 f1() 之前的整个程序状态,捕捉 f1() 的所有可能的异常,然后恢复原状态。

可是就算 f1()f2()能提供强保证,情况并不就此好转。设想假如f1()操作成功,程序状态再任何方面都可能有所改变,然后f2()抛出异常,这种情况下我们就需要把 f1()所做的改动也撤回。可是有些改动则是根本不能撤回的,例如对数据库已提交的(committed)改动,用户早就已经看到了。这些问题都会导致你对强烈保证望而却步,再加上 swap and copy本身也有效率问题,因为要生成拷贝,对拷贝对象执行操作,再替换原来的对象,这是一个费时费空间的操作。所以,不用什么时候都需要提供强烈保证,但你有能力实现时可以提供强烈保证。

当强烈保证不切实际时,你就必须提供基本保证。如果你实现了强烈保证,但付出的代价是巨大的,你可以退一步选择基本保证。对许多函数来说,“异常安全性之基本保证”是一个绝对通情达理的选择。

要设计异常安全的系统,首先从堵住资源泄漏开始,使用资源管理类,然后在这三种异常安全性中做出符合实际的选择,只有在使用异常不安全的老代码让你无路可走时才考虑零保证,最后记录下你的决定和原因,为用户和以后的维护考虑。

Note:

  • 异常安全的函数即使在抛出异常时也不会泄露资源,损坏数据结构。这种安全性有三种级别,基本保证,强列保证和不抛出保证
  • copy and swap是实现强保证的有效方法,但强烈保证并不是每个函数都可实现或具备实现意义的
  • 函数的异常安全性遵循木桶原理,函数的最强安全性取决于它所调用操作的最弱安全性

条款30:透彻了解 inlining 的内内外外

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值