条款29:为"异常安全"而努力是值得的
(Strive for exception-safe code.)
内容:
看了这款的内容,我对C++的难以控制的"脾气"又有了进一步的了解,C++的安全性问题一直是广大非C++程序
员所抨击C++语言"恶行"的主要方面,我们今天讨论的其"异常安全性"也是其复杂之处之一,看完这一款之后,你也许
会发觉你以前写的代码可能会给最终产品带来多大的"风险隐患",废话我就不多说了,开始进入今天的话题.
按照老规矩来,先看一个例子:假设有个class用来表现夹带背景图案的GUI菜单,这个class也要用于多线程
环境当中,所以我们考虑用了一个互斥器(mutex)作为并发控制(concurrency control)之用:
class PrettyMenu{
public:
...
void changeBackground(std::istream& imgSrc); //改变图片背景
...
private:
Mutex mutex_; //互斥器
Image* bgImage_; //目前的背景图像
int imageChangesCounts_; //背景图像被改变的次数
};
下面是一个可能的实现:
void PrettyMenu::changeBackground(std::istream& imgSrc){
lock(&mutex_); //取得互斥器
delete bgImage_; //摆脱旧的背景图像
++imageChangesCounts_; //修改变更次数
bgImage_ = new Image(imgSrc); //安装新的背景图像
unlock(&mutex_); //释放互斥器
}
从"异常安全性"的角度来看,这个真是个糟糕的实现版本,至少我们能够指出如下两点不足:(1)如果Image构造
函数抛出异常,那么mutex_就永远不能得到unlock;(2)此外如果构造没有成功,而imageChangesCounts_是在构造
之前执行,那么其自然就出现了"数据失真"的风险.
解决问题(1)我们可以用对象管理资源方法进行解决(条款13中提到),我们只要把imageChangesCounts_的自
增操作放在对象产生之后也就可以解决问题(2)了,该实现先修改如下:
void PrettyMenu::changeBackground(std::istream& imgSrc){
Lock(&mutex_); //获得互斥器并确保它稍后被释放
delete bgImage_;
bgImage_ = new Image(imgSrc);
++imageChangesCounts_;
}
进一步我们可以把删除操作放在智能指针内部完成:
class PrettyMenu{
...
std::tr1::shared_ptr<Image> bgImage_;
};
void PrettyMenu::changeBackground(std::istream& imgSrc){
Lock(&mutex_); //获得互斥器并确保它稍后被释放
bgImage_.reset(new Image(imgSrc));
++imageChangesCounts_;
}
代码是不是简洁多了,呵呵.看起来这样使得"异常安全机制"很完美嘛,不过"美中不足"的是:如果Image构造函数出
现异常,那么有可能输入流imageSrc的读取记号将被移走.这样的话,该函数也只能提供"基本的异常安全保证".什么是
"基本的异常安全保证",别急,挺我给你慢慢道来,作为异常安全函数必须提供以下三个保证之一:
■ 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下.
■ 强烈保证:如果异常被抛出,程序状态不改变.
■ 不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能.
理解了上面三种专业属于后,我们来把目光再次聚焦到上面changeBackground的代码实现上,看看现在的代码
,已经很好了呀,由于先前的那个小不足,使得我的这段代码还是没有达到"异常安全的强烈保证",我不放弃,四处寻找解
决方案终于有个一般化的设计策略可以达到这个目的,这个策略被称为"copy and swap",俗话说"名如其人",这个原则其
实就是:为你打算修改的对象保存一个副本,然后在该副本上修改.若修改过程中发生异常,问题不大,呵呵,因为原
对象状态没有被改变嘛.修改动作完成以后进行"副本与原对象互换"操作.真是个不错的方案,我心理不由的赞一个,下
面我们就开始改代码:
struct PMImpl{
std::tr1::shared_ptr<Image> bgImage_;
int imageChangesCounts_;
};
class PrettyMenu{
...
void changeBackground(std::istream& imgSrc){
using std::swap; //哒哒哒哒,条款25我们谈到这个swap的实现方法的喔!
Lock m1(&mutex_);
std::tr1::shared_ptr<PMImpl> pNewImpl(new PMImpl(*pImpl_)); //make copy
pNewImpl->bgImage_.reset(new Image(imgSrc));//handle copy
++pNew->imageChangesCounts_;
swap(pImpl_,pNewImpl); //swap
}
private:
Mutex mutex_;
std::tr1::shared_ptr<PMImpl> pImpl_;
};
NND,代码量又增多了,有得必有失嘛,想开点!我们注意到copy-and-swap的关键在于"修改对象数据副本,然后在
一个不抛异常的函数中将修改后的数据和原件置换",因此必须为每一个即将被改动的对象做一个副本,那得耗用你
可能无法(无意愿)供应的时间和空间.这是一个很实际的问题:我们都希望提供"强烈保证";当它可被实现时你的确应
该提供它,但"强烈保证"并非在任何时刻都显得实际.当强烈保证不切实际的时候,你就必须提供基本保证.在实际的开
发当中你可以为某些函数提供强烈保证,但效率和复杂度带来的成本会使得你不得不去放弃它,万一实际不可行,使
你退而求其次地只提供基本保证,任何人都不该因此责难你.对许多函数而言,"异常安全性之基本保证"是一个绝对通情达
理的选择.
累死我了,到此结束.
请记住:
■ 异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构被破坏.这样的函数区分为三种可能的保证:
基本型、强烈型、不抛异常型.
■ "强烈保证"往往能够以copy-and-swap实现出来,但"强烈保证"并非对所有函数都可实现或具备现实意义.
■ 函数提供的"异常安全保证"通常最高只等于其所调用之各个函数的"异常安全保证"中的最弱者.
条款29:为"异常安全"而努力是值得的
最新推荐文章于 2022-07-14 21:18:49 发布