您的位置:首页 > Web前端

[翻译] Effective C++, 3rd Edition, Item 29: 争取 exception-safe code(异常安全代码)(上)

2005-08-24 00:53 666 查看
Item 29: 争取 exception-safe code(异常安全代码)

作者:Scott Meyers

译者:fatalerror99 (iTePub's Nirvana)

发布:http://blog.csdn.net/fatalerror99/

exception safety(异常安全)有点像 pregnancy(怀孕)……但是,请把这个想法先保留一会儿。我们还不能真正地议论 reproduction(生育),直到我们排除万难渡过 courtship(求爱时期)。(此段作者使用的 3 个词均有双关含义,pregnancy 也可理解为富有意义,reproduction 也可理解为再现,再生,courtship 也可理解为争取,谋求。为了与后面的译文对应,故按照现在的译法。——译者注)

假设我们有一个 class,代表带有背景图像的 GUI 菜单。这个 class 被设计用于一个 threaded environment(多线程环境),所以它有一个用于 concurrency control(并发控制)的 mutex(互斥体):

class PrettyMenu {
public:
...
void changeBackground(std::istream& imgSrc); // change background
... // image

private:

Mutex mutex; // mutex for this object

Image *bgImage; // current background image
int imageChanges; // # of times image has been changed
};

考虑这个 PrettyMenu 的 changeBackground 函数的可能的实现:

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
lock(&mutex); // acquire mutex (as in Item 14)

delete bgImage; // get rid of old background
++imageChanges; // update image change count
bgImage = new Image(imgSrc); // install new background

unlock(&mutex); // release mutex
}

从 exception safety(异常安全)的观点看,这个函数烂到了极点。exception safety(异常安全)有两条要求,而这里全都没有满足。

当一个 exception(异常)被抛出,exception-safe functions(异常安全函数)应该:

Leak no resources(没有资源泄露)。上面的代码没有通过这个测试,因为如果 "new Image(imgSrc)" 表达式引发一个 exception(异常),对 unlock 的调用就永远不会执行,而那个 mutex(互斥体)也将被永远挂起。

Don't allow data structures to become corrupted(不允许数据结构被破坏)。如果 "new Image(imgSrc)" throws(抛出异常),bgImage 被留下来指向一个已删除 object。另外,尽管并没有将一张新的图像设置到位,imageChanges 也已经被增加。(在另一方面,旧的图像被明确地删除,所以我料想你会争辩说图像已经被“改变”了。)

规避 resource leak(资源泄露)问题比较容易,因为 Item 13 解释了如何使用 objects 管理资源,而 Item 14 又引进了 Lock class 作为一个确保互斥体被及时恰当地释放的方法:

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex); // from Item 14: acquire mutex and
// ensure its later release
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}

关于像 Lock 这样的 resource management classes(资源管理类)的最好的事情之一是它们通常会使函数变短。看到如何使对 unlock 的调用不再需要了吗?作为一个一般的规则,更少的代码就是更好的代码,因为在改变的时候这样可以较少误入歧途并较少产生误解。

随着 resource leak(资源泄露)被我们甩在身后,我们可以把我们的注意力集中到 data structure corruption(数据结构被破坏)的问题。在这里我们有一个选择,但是在我们能选择之前,我们必须先面对定义我们的选择的术语。

exception-safe functions(异常安全函数)提供下述三种保证之一:

函数提供 the basic guarantee(基本保证),允诺如果一个异常被抛出,程序中剩下的每一件东西都处于合法状态。没有 objects 或数据结构被破坏,而且所有的 objects 都处于内部调和状态(所有的 class invariants(类不变量)都被满足)。然而,程序的精确状态可能是不可预言的。例如,我们可以重写 changeBackground,以便于在一个异常被抛出时,PrettyMenu object 可以继续保留原来的背景图像,或者它可以持有某些缺省的背景图像,但是客户无法预知到底是哪一个。(为了查明这一点,他们大概必须调用某个可以告诉他们当前背景图像是什么的 member function。)

函数提供 the strong guarantee(强力保证),允诺如果一个异常被抛出,程序的状态不会发生变化。调用这样的函数在感觉上是 atomic(原子)的,如果它们成功了,它们就完全成功,如果它们失败了,程序的状态就像它们从没有被调用过一样。

与提供 strong guarantee(强力保证)的函数一起工作比与只提供 basic guarantee(基本保证)的函数一起工作更加容易,因为调用提供 strong guarantee(强力保证)的函数之后,仅有两种可能的程序状态:像预期一样成功执行了函数,或者继续保持函数被调用时当时的状态。与之相比,如果调用一个只提供 basic guarantee(基本保证)的函数引发了异常,程序可能存在于任何合法的状态。

函数提供 the nothrow guarantee(不抛出保证),允诺决不抛出异常,因为它们只做它们保证能做到的。所有对 built-in types(内建类型)(例如,ints,指针,等等)的操作都是 nothrow(不抛出)的(也就是说,提供 nothrow guarantee(不抛出保证))。这是 exception-safe code(异常安全代码)中必不可少的基础构件。

假设一个带有 empty exception specification(空异常规格)的函数是不抛出的似乎是合理的,但这不是一定成立的。例如,考虑这个函数:

int doSomething() throw(); // note empty exception spec.

这并不是说 doSomething 永远都不会抛出异常;而是说如果 doSomething 抛出一个异常,它就是一个严重的错误,应该调用 unexpected function [1]。实际上,doSomething 可能根本不提供任何异常保证。一个函数的声明(如果有的话,也包括它的 exception specification(异常规格))不能告诉你一个函数是否正确,是否可移植,或是否高效,而且,即便有,它也不能告诉你它会提供哪一种 exception safety guarantee(异常安全保证)。所有这些特性都由函数的实现决定,而不是它的声明。

[1] 关于 unexpected function 的资料,可以求助于你中意的搜索引擎或包罗万象的 C++ 课本。(你或许有幸搜到 set_unexpected,这个函数用于指定 unexpected function。)

exception-safe code(异常安全代码)必须提供上述三种保证中的一种。如果它没有提供,它就不是 exception-safe(异常安全)的。于是,选择就在于决定你写的每一个函数究竟要提供哪种保证。除非要处理 exception-unsafe(异常不安全)的遗留代码(本 Item 稍后我们要讨论这个问题),只有当你的最高明的需求分析团队为你的应用程序识别出的一项需求就是泄漏资源以及运行于被破坏的数据结构之上时,不提供 exception safety guarantee(异常安全保证)才能成为一个选项。

作为一个一般性的规则,你应该提供实际可达到的最强力的保证。从 exception safety(异常安全)的观点看,nothrow functions(不抛出的函数)是极棒的,但是在 C++ 的 C 部分之外不调用可能抛出异常的函数简直就是寸步难行。使用动态分配内存的任何东西(例如,所有的 STL containers)如果不能找到足够的内存来满足一个请求(参见 Item 49),在典型情况下,它就会抛出一个 bad_alloc 异常。只要你能做到就提供 nothrow guarantee(不抛出保证),但是对于大多数函数,选择是在基本保证和强力保证之间的。

在 changeBackground 的情况下,提供 almost(差不多)的 strong guarantee(强力保证)并不困难。首先,我们将 PrettyMenu 的 bgImage data member 的类型从一个 built-in Image* pointer(指针)改变为 Item 13 中描述的 smart resource-managing pointers(智能资源管理指针)中的一种。坦白地讲,在预防资源泄漏的基本原则上,这完全是一个好主意。它帮助我们提供 strong exception safety guarantee(强力异常安全保证)的事实进一步加强了 Item 13 的论点——使用 objects(诸如 smart pointers(智能指针))管理资源是良好设计的基础。在下面的代码中,我展示了 tr1::shared_ptr 的使用,因为当进行通常的拷贝时它的行为更符合直觉,这使得它比 auto_ptr 更可取。

第二,我们重新排列 changeBackground 中的语句,以便于直到图像发生变化,才增加 imageChanges。作为一个一般规则,这是一个很好的策略——直到某件事情真正发生了,再改变一个 object 的状态来表示某事已经发生。

这就是修改之后的代码:

class PrettyMenu {
...
std::tr1::shared_ptr<Image> bgImage;
...
};

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex);

bgImage.reset(new Image(imgSrc)); // replace bgImage's internal
// pointer with the result of the
// "new Image" expression
++imageChanges;
}

注意这里不再需要手动删除旧的图像,因为这些已经由 smart pointer(智能指针)在内部处理了。此外,只有当新的图像被成功创建了删除行为才会发生。更准确地说,只有当 tr1::shared_ptr::reset 函数的参数("new Image(imgSrc)" 的结果)被成功创建了,这个函数才会被调用。只有在 reset 的调用中才会使用 delete,所以如果这个函数从来不曾进入,delete 就从来不曾使用。同时请注意一个管理资源(动态分配的 Image)的 object (tr1::shared_ptr) 的使用又一次缩短了 changeBackground 的长度。

正如我所说的,这两处改动 almost(差不多)有能力使 changeBackground 提供 strong exception safety guarantee(强力异常安全保证)。美中不足的是什么呢?参数 imgSrc。如果 Image constructor(构造函数)抛出一个异常,input stream(输入流)的读标记就有可能已经被移动,而这样的移动就成为一个对程序的其它部分来说可见的状态变化。直到 changeBackground 着手解决这个问题之前,它只能提供 basic exception safety guarantee(基本异常安全保证)。

(本篇未完,点击此处,接下篇)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐