C++ 为 异常安全 而努力是值得的

本文探讨了在C++中如何实现异常安全的代码设计,通过使用互斥器和智能指针等手段来确保函数即使在异常情况下也能正确释放资源,并介绍了一种名为copy-and-swap的技术来实现更强大的异常安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

20180319 C++ 为 异常安全 而努力是值得的


假设有个class用来表现夹带背景图案的GUI菜单。这个class希望用于多线程环境,所以他有个互斥器(mutex)作为并发控制(concurrency control)之用:
class  PrettyMenu{
public:
  ...
  void changeBackground(std::istream& imgSrc);//改变背景颜色
  ...
private:
  Mutex mutex;//互斥器
  Image* bgImage;//目前的背景图案
  int imageChange;//背景图像被改变的次数
}


下面是PrettyMenu的changeBlackground函数的一个可能实现:
void  PrettyMenu::changeBlackground(std::istream& imgSrc)
{
  lock(&mutex);//取得互斥器
  delete bgImage;//摆脱旧的背景图像
  ++imageChange;//修改图像变更次数
  bgImage = new Image(imgSrc);//安装新的背景图像
  unlock(&mutex);//释放互斥器
}




从“异常安全性”的观点来看,这个函数很糟。“异常安全”有两个条件,而这个函数没有满足其中任何一条。当异常被抛出时,带有异常安全性的函数会:
1、不泄露任何资源。上述代码没有做到这一点,因为一旦“newImage(imgSrc)”导致异常,对unlock的调用就绝对不会执行,于是互斥器就永远被把持住了。
2、强烈保证:不允许数据败坏。若“newImage(imgSrc)”抛出异常,bgImage就是指向一个已经被删除的对象,imageChange也已被累加,而其实并没有新的图像被成功安装起来。


解决资源泄露的问题可以使用“ 导入Lock class作为一种"确保互斥器被及时释放"的方法 ”:
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
  Lock m1(&mutex);//
  delete bgImage;
  ++imageChanges;
  bgImage = new Image(imgSrc);
}


异常安全函数(Exception-safe function)提供以下三个保证之一:
1、基本承诺:若异常被抛出,程序内的任何事物仍然保持在有效状态下。
2、若异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:若函数成功,就是完全成功,若函数失败,程序会回复到“调用函数之前”的状态。
3、不抛掷保证:承诺绝不抛出异常,因为他们总是能够完成他们原先承诺的功能。作用于内置类型(例如ints,指针等)身上的所有操作都提供nothrow保证。








考虑以下函数,eg:


int doSomething() throw();
这并不是说,doSomething绝对不会抛出异常,而是说doSomething抛出异常,将是严重错误,会有你意想不到的函数被调用。
可能的话,请尽可能提供nothrow保证,但对大部分函数而言,抉择往往落在基本保证和强烈保证之间。


以下的改变几乎足够让changeBackground提供强烈的异常安全保证:
class PrettyMenu{
  ...
  std::tr1:;shared_ptr<Image> bgImage;
  ...
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
  lock(&mutex);
  bgImage.reset(new Image(imgSrc));//以“new Image”的执行结果
                                   //设定bgImage内部指针
  ++imageChanges;
}




有一个一般化的设计策略很典型地会导致强烈保证,很值得熟悉他,这个策略叫 copy and swap,原则很简单:
为你打算修改的对象(原件)做出一份副本,然后在那份副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未修改状态。待所有修改都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。


实际上通常是将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象(implementation object,即副本)。这种手法常被称为pimpl idiom,对于PrettyMenu而言,典型写法如下:
struct PMImpl{
  std::tr1::shared_ptr<Image> bgImage;
  int imageChanges;
};
class PrettyMenu{
  ...
private:
  Mutex mutex;
  std::tr1::shared_ptr<PMImpl> pImpl;
};
  
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
  using std::swap;
  Lock m1(&mutex);//获得mutex的副本数据
  std::tr1::shared_ptr<PMImpl>
  pNew(new PMImpl(*pImpl));
  
  pNew->bgImage.reset(new Image(imgSrc));//修改副本
  ++pNew->imageChanges;
  swap(pImpl,pNew);//置换(swap)数据,释放mutex
}


注意:
1、异常安全函数(Exception-safe function)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
2、“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备显示意义。
3、函数提供的“异常安全保证”通常最高只等于其所调用的各个函数的“异常安全保证”中的最弱着。


























评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值