您的位置:首页 > Web前端

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

2005-08-25 23:38 435 查看
(点击此处,接上篇)

然而,让我们把它放在一边,并且依然假装 changeBackground 可以提供 strong guarantee(强力保证)。(我确信你至少能提出一种方法让它做到这一点,或许可以通过将它的参数类型从一个 istream 变成包含图像数据的文件的文件名。)有一种典型的产生 strong guarantee(强力保证)的通用设计策略,而熟悉它是非常必要的。这个策略被称为 "copy and swap"。在原理上,它很简单。先做出一个你要改变的 object 的拷贝,然后在这个拷贝上做出全部所需的改变。如果改变过程中的某些操作抛出了异常,原来的 object 保持不变。在所有的改变全部成功之后,将原来的和被改变的 object 和在一个 non-throwing(不抛出)的操作中进行交换。

这通常通过这种方法实现:将整个对象的全部数据从“真正的” object 中放入到一个单独的执行 object 中,然后将一个指向执行 object 的指针交给真正的 object。这通常被称为 "pimpl idiom",Item 31 描述了它的一些细节。对于 PrettyMenu 来说,它一般就像这样:

struct PMImpl { // PMImpl = "PrettyMenu
std::tr1::shared_ptr<Image> bgImage; // Impl."; see below for
int imageChanges; // why it's a struct
};

class PrettyMenu {
...

private:
Mutex mutex;
std::tr1::shared_ptr<PMImpl> pImpl;
};

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
using std::swap; // see Item 25

Lock ml(&mutex); // acquire the mutex

std::tr1::shared_ptr<PMImpl> // copy obj. data
pNew(new PMImpl(*pImpl));

pNew->bgImage.reset(new Image(imgSrc)); // modify the copy
++pNew->imageChanges;

swap(pImpl, pNew); // swap the new
// data into place

} // release the mutex

在这个例子中,我选择将 PMImpl 做成一个 struct,而不是 class,因为通过让 pImpl 是 private 就可以确保 PrettyMenu 数据的封装。将 PMImpl 做成一个 class 尽管少了一些便利性,却没有增加什么好处。(这也会使有 object-oriented 洁癖者走投无路。)如果你愿意,PMImpl 可以嵌套在 PrettyMenu 内部,像这样的打包问题与我们这里所关心的写 exception-safe code(异常安全代码)之间没有什么关系。

copy-and-swap 策略是一种要么全部改变,要么丝毫不变一个 object 的状态的极好的方法,但是,在通常情况下,它不能保证全部函数都是 strongly exception-safe(强力异常安全)的。为了弄清原因,考虑一个 changeBackground 的抽象化身—— someFunc,它使用了 copy-and-swap,但是它包含了对另外两个函数(f1 和 f2)的调用:

void someFunc()
{
... // make copy of local state
f1();
f2();
... // swap modified state into place
}

很明显,如果 f1 或 f2 低于 strongly exception-safe(强力异常安全),someFunc 就很难成为 strongly exception-safe(强力异常安全)的。例如,假设 f1 仅提供 basic guarantee(基本保证)。为了让 someFunc 提供 strong guarantee(强力保证),它必须写代码在调用 f1 之前测定整个程序的状态,并捕捉来自 f1 的所有异常,然后恢复到原来的状态。

即使 f1 和 f2 都是 strongly exception safe(强力异常安全)的,事情也好不到哪去。毕竟,如果 f1 运行完成,程序的状态已经发生了毫无疑问的变化,所以如果随后 f2 抛出一个异常,即使 f2 没有改变任何东西,程序的状态也已经和调用 someFunc 时不同。

问题在于副作用。只要函数仅对局部状态起作用(例如,someFunc 仅仅影响调用它的那个 object 的状态),它提供 strong guarantee(强力保证)就相对容易。当函数有作用于非局部数据的副作用,它就会困难得多。例如,如果调用 f1 的副作用是一个数据库被改变,让 someFunc 成为 strongly exception-safe(强力异常安全)就非常困难。一般情况下,没有办法撤销已经提交的数据库变化,其他数据库客户可能已经看见了数据库的新状态。

类似这样的问题可能会阻止你为一个函数提供 strong guarantee(强力保证),即使你希望去做。另一个问题是性能。copy-and-swap 的要点是这样一个想法:改变一个 object 的数据的拷贝,然后在一个 non-throwing(不抛出)的操作中将原来的和被改变的数据进行交换。这就需要做出每一个将被改变的 object 的拷贝,这可能会用到你不能或不情愿动用的时间和空间。strong guarantee(强力保证)是非常值得的,当它可行时你应该提供它,除非在它不能 100% 可行的时候。

当它不可行时,你就必须提供 basic guarantee(基本保证)。在实践中,你可能会发现你能为某些函数提供 strong guarantee(强力保证),但是性能和复杂度的成本使得它难以用于大量的其它函数。只要你做过只要可行就提供 strong guarantee(强力保证)的合理的努力,当你只提供了 basic guarantee(基本保证)时,就没有人会因此而站在批评你的立场上。对于很多函数来说,basic guarantee(基本保证)是一个完全合理的选择。

如果你写了一个根本没有提供 exception-safety guarantee(异常安全保证)的函数,事情就不同了,因为在这一点上有罪推定是合情合理的,直到你证明自己是清白的。你应该写出 exception-safe code(异常安全代码)。除非你能做出有说服力的答辩。请再次考虑调用了函数 f1 和 f2 的 someFunc 的实现。假设 f2 根本没有提供 exception safety guarantee(异常安全保证),甚至没有 basic guarantee(基本保证)。这就意味着如果 f2 发生一个异常,程序可能会在 f2 内部泄漏资源。这也意味着 f2 可能会破坏数据结构,例如,有序数组可能不再有序,正在从一个数据结构传递给另一个数据结构去的 objects 可能会丢失,等等。没有任何办法可以让 someFunc 能弥补这些问题。如果 someFunc 调用的函数不提供 exception-safety guarantees(异常安全保证),someFunc 本身就不能提供任何保证。

请允许我回到怀孕。一个女性或者怀孕或者没有。部分地怀孕是绝不可能的。与此相似,一个软件系统或者是 exception-safe(异常安全)的或者不是。没有像 partially exception-safe system(部分异常安全系统)这样的东西。一个系统即使只有一个独立函数不是 exception-safe(异常安全)的,那么系统作为一个整体就不是 exception-safe(异常安全)的,因为调用那个函数可能导致泄漏资源和破坏数据结构。不幸的是,很多 C++ 的遗留代码在写的时候没有留意 exception safety(异常安全),所以现在的很多系统都不是 exception-safe(异常安全)的。它们混合了用 exception-unsafe(非异常安全)的风格书写的代码。

没有理由让事情的这种状态永远持续下去。当书写新的代码或修改已有代码时,要仔细考虑如何使它 exception-safe(异常安全)。从使用 objects 管理资源开始(还是参见 Item 13)。这样可以防止资源泄漏。接下来,决定三种 exception safety guarantees(异常安全保证)中的哪一种是你能够为你写的每一个函数实际提供的最强的保证,只有当你不调用遗留代码就别无选择的时候,才能满足于没有保证。既为你的函数的客户也为将来的维护人员,文档化你的决定。一个函数的 exception-safety guarantee(异常安全保证)是它的接口的可见部分,所以你应该谨慎地选择它,就像你谨慎地选择一个函数接口的其它方面。

四十年前,到处都是 goto 的代码被尊为最佳实践。现在我们为书写结构化控制流程而奋斗。二十年前,全局可访问数据被尊为最佳实践。现在我们为封装数据而奋斗,十年以前,写函数时不必考虑异常的影响被尊为最佳实践。现在我们为写 exception-safe code(异常安全代码)而奋斗。

时光在流逝。我们生活着。我们学习着。

Things to Remember

即使当异常被抛出时,exception-safe functions(异常安全函数)不会泄露资源,也不允许数据结构被破坏。这样的函数提供 basic(基本)的,strong(强力)的,或者 nothrow(不抛出)保证。

strong guarantee(强力保证)经常可以通过 copy-and-swap 被实现,但是 strong guarantee(强力保证)并非对所有函数都可行。

一个函数通常能提供的保证不会强于他所调用的函数中最弱的保证。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐