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

(点击此处,接上篇)

然而,让我们把它放在一边,并且依然假装 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,但是它包含了对另外两个函数(f1f2)的调用:

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

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

即使 f1f2 都是 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(异常安全代码)。除非你能做出有说服力的答辩。请再次考虑调用了函数 f1f2someFunc 的实现。假设 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(强力保证)并非对所有函数都可行。
  • 一个函数通常能提供的保证不会强于他所调用的函数中最弱的保证。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值