假设我们有一个 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
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
delete bgImage; // get rid of old background
++imageChanges; // update image change count
bgImage = new Image(imgSrc); // install new background
int imageChanges; // # of times image has been changed
};
考虑这个 PrettyMenu 的 changeBackground 函数的可能的实现:
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
lock(&mutex); // acquire mutex
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 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) ;
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}
{
Lock ml(&mutex) ;
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(内建类型)(例如,
int
s,指针,等等)的操作都是 nothrow(不抛出)的(也就是说,提供 nothrow guarantee(不抛出保证))。这是 exception-safe code(异常安全代码)中必不可少的基础构件。
假设一个带有 empty exception specification(空异常规格)的函数是不抛出的似乎是合理的,但这不是一定成立的。例如,考虑这个函数:
int doSomething()
throw()
; // note empty exception spec.
这并不是说
doSomething
永远都不会抛出异常;而是说如果
doSomething
抛出一个异常,它就是一个严重的错误,应该调用
unexpected
function。实际上,
doSomething
可能根本不提供任何异常保证。一个函数的声明(如果有的话,也包括它的 exception specification(异常规格))不能告诉一个函数是否正确,是否可移植,或是否高效,而且,即便有,它也不能告诉它会提供哪一种 exception safety guarantee(异常安全保证)。所有这些特性都由函数的实现决定,而不是它的声明。
exception-safe code(异常安全代码)必须提供上述三种保证中的一种。如果它没有提供,它就不是 exception-safe(异常安全)的。于是,选择就在于决定你写的每一个函数究竟要提供哪种保证。除非要处理 exception-unsafe(异常不安全)的遗留代码,只有当你的最高明的需求分析团队为你的应用程序识别出的一项需求就是泄漏资源以及运行于被破坏的数据结构之上时,不提供 exception safety guarantee(异常安全保证)才能成为一个选项。
作为一个一般性的规则,应该提供实际可达到的最强力的保证。从 exception safety(异常安全)的观点看,nothrow functions(不抛出的函数)是极棒的,但是在 C++ 的 C 部分之外不调用可能抛出异常的函数简直就是寸步难行。使用动态分配内存的任何东西(例如,所有的 STL containers)如果不能找到足够的内存来满足一个请求,在典型情况下,它就会抛出一个
bad_alloc
异常。只要能做到就提供 nothrow guarantee(不抛出保证),但是对于大多数函数,选择是在基本保证和强力保证之间的。
在
changeBackground
的情况下,提供
almost
(差不多)的 strong guarantee(强力保证)并不困难。首先,我们将
PrettyMenu
的
bgImage
data member 的类型从一个 built-in
Image*
pointer(指针)改变为smart resource-managing pointers(智能资源管理指针)中的一种。坦白地讲,在预防资源泄漏的基本原则上,这完全是一个好主意。它帮助我们提供 strong exception safety guarantee(强力异常安全保证)的事实进一步加强了这一论点——使用 objects(诸如 smart pointers(智能指针))管理资源是良好设计的基础。在下面的代码中,展示了
tr1::shared_ptr
的使用,因为当进行通常的拷贝时它的行为更符合直觉,这使得它比
auto_ptr
更可取。
第二,我们重新排列
changeBackground
中的语句,以便于直到图像发生变化,才增加
imageChanges
。作为一个一般规则,这是一个很好的策略——直到某件事情真正发生了,再改变一个 object 的状态来表示某事已经发生。
这就是修改之后的代码:
class PrettyMenu {
...
std::tr1::shared_ptr<Image> bgImage;
...
};
...
std::tr1::shared_ptr<Image> bgImage;
...
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex);
{
Lock ml(&mutex);
bgImage.reset(new Image(imgSrc))
; // replace bgImage's internal
// pointer with the result of the
// "new Image" expression
++imageChanges ;
}
// 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(基本异常安全保证)。
然而,让我们把它放在一边,并且依然假装
changeBackground
可以提供 strong guarantee(强力保证)。(确信至少有一种方法让它做到这一点,或许可以通过将它的参数类型从一个
istream
变成包含图像数据的文件的文件名。)有一种典型的产生 strong guarantee(强力保证)的通用设计策略,而熟悉它是非常必要的。这个策略被称为 "copy and swap"。在原理上,它很简单。先做出一个要改变的 object 的拷贝,然后在这个拷贝上做出全部所需的改变。如果改变过程中的某些操作抛出了异常,原来的 object 保持不变。在所有的改变全部成功之后,将原来的和被改变的 object 和在一个 non-throwing(不抛出)的操作中进行交换。
这通常通过这种方法实现:将整个对象的全部数据从“真正的” object 中放入到一个单独的执行 object 中,然后将一个指向执行 object 的指针交给真正的 object。这通常被称为 "pimpl idiom"。对于
PrettyMenu
来说,它一般就像这样:
struct PMImpl
{ // PMImpl = "PrettyMenu
std::tr1::shared_ptr<Image> bgImage; // Impl."; see below for
int imageChanges; // why it's a struct
};
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 ;
};
Mutex mutex;
std::tr1::shared_ptr<PMImpl> pImpl ;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
using std::swap;
{
using std::swap;
Lock ml(&mutex); // acquire the mutex
std::tr1::shared_ptr<PMImpl> //
copy
obj. data
pNew(new PMImpl(*pImpl));
pNew(new PMImpl(*pImpl));
pNew->bgImage.reset(new Image(imgSrc)); // modify the copy
++pNew->imageChanges;
++pNew->imageChanges;
swap(pImpl, pNew); //
swap
the new
// data into place
// data into place
} // release the mutex
在这个例子中,选择将
PMImpl
做成一个 struct,而不是 class,因为通过让
pImpl
是 private 就可以确保
PrettyMenu
数据的封装。将
PMImpl
做成一个 class 尽管少了一些便利性,却没有增加什么好处。如果愿意,
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
}
{
... // 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 管理资源开始。这样可以防止资源泄漏。接下来,决定三种 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(强力保证)并非对所有函数都可行。
一个函数通常能提供的保证不会强于他所调用的函数中最弱的保证。