异常安全的代码是指,满足两个条件
1异常中立性 :
是指当你的代码(包括你调用的代码)引发异常时,这个异常 能保持原样传递到外层调用代码。(异常中立,就是指任何底层的异常都会抛出到上层,也就相当于是异常透明的。)
2.异常安全性:
- 抛出异常后,资源不泄露,
- 抛出异常后,不会使原有数据恶化(例如正常指针变野指针)
- 少些try catch,因为大量的try catch会影响代码逻辑。导致代码丑陋混乱不优雅
一段代码要具有异常安全性,必须同时具有异常中立性和一定等级的异常安全性保证
异常安全的等级一般有:
1,函数提供基本保证(the basic guarantee)(不会发生内存泄漏并且程序内的每个对象都处在合法的状态,没有流错位,没有野指针,但是不是每个对象的精确状态是可以预期的,如可能发生异常后,指针处在空指针状态,或者某种默认状态,但是客户无法预知到底是哪一个),对于达成基本保证可以多使用智能指针来管理资源
2,函数提供强力保证(the strong guarantee),强力保证含义是,成功或者回滚保证,发生异常的函数对程序来说,没有任何改动,提供发生异常时候的回滚机制。
调用提供强力保证的函数之后,仅有两种可能的程序状态:像预期一样成功执行了函数,或者函数回滚继续保持函数被调用时当时的状态。与之相比,如果调用只提供基本保证的函数引发了异常,程序可能存在于任何合法的状态。
函数提供强力保证的有效解决办法是
copy-and-swap:
先做出一个你要改变的对象的copy,然后在这个copy上做出全部所需的改变。如果改变过程中的某些操作抛出了异常,最初的对象保持不变。在所有的改变完全成功之后,将被改变的对象和最初的对象在一个不会抛出异常的操作中进行swap。
3. 函数有不抛出保证(the nothrow guarantee),对于所有对内建类型(例如,ints,指针,等等)的操作都是不抛出(nothrow)的(也就是说,提供不抛出保证)。这是异常安全代码中必不可少的基础构件。
注意事项:
异常安全 最关键的是:swap ctor dctor 不发生异常保证,只有成功或者终止程序两种状态
一个函数的异常安全等级,是取决于它所调用的函数中最低异常安全等级的函数。
C++11新增了noexcept关键字,在void func() noexcept{}noexcept保证了这个函数不会抛出异常,只有终止程序和成功执行两种状态。
noexcept可以接受一个常量表达式,noexcept(constexpr。。。)当常量表达式为转换为true说明该函数保证不抛出异常。
从异常安全的观点看,不抛出的函数(nothrow functions)是极好的,但是在 C++ 的 C 部分之外部不调用可能抛出异常的函数简直就是寸步难行。使用动态分配内存的任何东西(例如,所有的 STL 容器)如果不能找到足够的内存来满足一个请求,在典型情况下,它就会抛出一个 bad_alloc 异常。只要你能做到就提供不抛出保证,但是对于大多数函数,选择是在基本的保证和强力的保证之间的。
但是,不是所有函数都能做出异常保证的,考虑这样一个函数,函数内部的函数内是一个对数据库的操作,一旦异常发生,难以撤销对数据库的更改。如果想对这样的函数做到异常的strong guarantee保证,就是非常困难度事情。
所以对于只对局部变量改变的函数保证异常安起会相对比较容易。如果函数的操作中牵扯到全局变量等等,就变得困难的多。
解决异常安全的好办法:
1,多使用RAII,使用智能指针来管理内存。由于unwind机制的保证,当异常发生时,函数栈内已构造的局部对象的析构函数会被一一调用,在析构函数内释放资源,也就杜绝了内存泄漏的问题。
2,做好程序设计。特别是异常发生时的回滚机制的正确使用,copy-and-swap是有效的方法。
3,注意需要异常保证的函数内部的调用函数,异常安全等级是以有最低等级异常保证的函数确定的。
一个系统即使只有一个函数不是异常安全的,那么系统作为一个整体就不是异常安全的,因为调用那个函数可能发生泄漏资源和恶化数据结构。
4,对于一些需要保证性能的程序,在提供基本的异常安全时,要注意,栈解退机制只是调用析构函数,对于内置类型的操作不会被回滚,所以。像起累加器作用的一些内置类型变量,应该注意在函数成功执行后再进行累加。避免数据结构恶化。重新分配资源给原本已经持有资源的变量,应该先清空释放变量的资源,指针再设置为nullptr,防止资源重新分配过程中抛出异常,导致指针变为野指针的问题。
5,流对象,资源对象,new对象,不应该直接作为参数,一旦抛出异常,就可能会导致严重的问题,函数也许会被错误的执行,资源也许会泄漏。对于函数参数和函数内使用的全局变量,应该保证在进入函数内部是是正常状态。
6.减少全局变量的使用,对包含全局变量的函数做异常安全是比较困难的事情,栈解退也只对局部变量起效果。
7,如果不知道如何处理异常,就不要捕获异常,直接终止比吞掉异常不处理要好
8.保证 构造 析构 swap不会失败
这里有个注意事项:
在构造函数中,如果抛出异常,是不会调用当前正在构造的类的析构函数的,因为当前正在构造的类没有构造完成,只会析构已经构造完成成员和父类,So,极易导致内存泄漏,这里要谨慎处理,使用RAII,智能指针,noexcept保证不会抛出异常和恶化数据。
参考文章:
1:对象生死劫-构造函数和析构函数异常
http://blog.csdn.net/leadzen/article/details/1783116
2:C++箴言:争取异常安全的代码
http://dev.yesky.com/490/2087990.shtml
异常安全(Exception safety)有点像怀孕(pregnancy)……但是,请把这个想法先控制一会儿。我们还不能真正地议论生育(reproduction),直到我们排除万难渡过求爱时期(courtship)。(此段作者使用的 3 个词均有双关含义,pregnancy 也可理解为富有意义,reproduction 也可理解为再现,再生,courtship 也可理解为争取,谋求。为了与后面的译文对应,故按照现在的译法。——译者注)
假设我们有一个类,代表带有背景图像的 GUI 菜单。这个类被设计成在多线程环境中使用,所以它有一个用于并行控制(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 } |
从异常安全的观点看,这个函数烂到了极点。异常安全有两条要求,而这里全都没有满足。
当一个异常被抛出,异常安全的函数应该:
·没有资源泄露。上面的代码没有通过这个测试,因为如果 "new Image(imgSrc)" 表达式产生一个异常,对 unlock 的调用就永远不会执行,而那个互斥体也将被永远挂起。
·不允许数据结构恶化。如果 "new Image(imgSrc)" 抛出异常,bgImage 被遗留下来指向一个被删除对象。另外,尽管并没有将一张新的图像设置到位,imageChanges 也已经被增加。(在另一方面,旧的图像被明确地删除,所以我料想你会争辩说图像已经被“改变”了。)
规避资源泄露问题比较容易,我们以前解释了如何使用对象管理资源,也讨论了引进 Lock 类作为一种时尚的确保互斥体被释放的方法:
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 这样的资源管理类的最好的事情之一是它们通常会使函数变短。看到对 unlock 的调用不再需要了吗?作为一个一般的规则,更少的代码就是更好的代码。因为在改变的时候这样可以较少误入歧途并较少产生误解。
随着资源泄露被我们甩在身后,我们可以把我们的注意力集中到数据结构恶化。在这里我们有一个选择,但是在我们能选择之前,我们必须先面对定义我们的选择的术语。 异常安全函数提供下述三种保证之一:
·函数提供基本保证(the basic guarantee),允诺如果一个异常被抛出,程序中剩下的每一件东西都处于合法状态。没有对象或数据结构被破坏,而且所有的对象都处于内部调和状态(所有的类不变量都被满足)。然而,程序的精确状态可能是不可预期的。例如,我们可以重写 changeBackground,以致于如果一个异常被抛出,PrettyMenu 对象可以继续保留原来的背景图像,或者它可以持有某些缺省的背景图像,但是客户无法预知到底是哪一个。(为了查明这一点,他们大概必须调用某个可以告诉他们当前背景图像是什么的成员函数。)
·函数提供强力保证(the strong guarantee),允诺如果一个异常被抛出,程序的状态不会发生变化。调用这样的函数在感觉上是极其微弱的,如果它们成功了,它们就完全成功,如果它们失败了,程序的状态就像它们从没有被调用过一样。
·与提供强力保证的函数一起工作比与只提供基本保证的函数一起工作更加容易,因为调用提供强力保证的函数之后,仅有两种可能的程序状态:像预期一样成功执行了函数,或者继续保持函数被调用时当时的状态。与之相比,如果调用只提供基本保证的函数引发了异常,程序可能存在于任何合法的状态。
函数提供不抛出保证(the nothrow guarantee),允诺决不抛出异常,因为它们只做它们答应要做的。所有对内建类型(例如,ints,指针,等等)的操作都是不抛出(nothrow)的(也就是说,提供不抛出保证)。这是异常安全代码中必不可少的基础构件。
假定一个带有空的异常规格(exception specification)的函数是不抛出的似乎是合理的,但这不一定正确的。例如,考虑这个函数:
int doSomething() throw(); // note empty exception spec. |
这并不是说 doSomething 永远不会抛出异常;而是说如果 doSomething 抛出一个异常,它就是一个严重的错误,应该调用 unexpected 函数 [1]。实际上,doSomething 可能根本不提供任何异常保证。一个函数的声明(如果有的话,也包括它的异常规格(exception specification))不能告诉你一个函数是否正确,是否可移植,或是否高效,而且,即便有,它也不能告诉你它会提供哪一种异常安全保证。所有这些特性都由函数的实现决定,而不是它的声明能决定的。
[1] 关于 unexpected 函数的资料,可以求助于你中意的搜索引擎或包罗万象的 C++ 课本。(你或许有幸搜到 set_unexpected,这个函数用于指定 unexpected 函数。)
异常安全函数必须提供上述三种保证中的一种。如果它没有提供,它就不是异常安全的。于是,选择就在于决定你写的每一个函数究竟要提供哪种保证。除非要处理遗留下来的非异常安全的代码(稍后我们要讨论这个问题),只有当你的最高明的需求分析团队为你的应用程序识别出的一项需求就是泄漏资源以及运行于被破坏的数据结构之上时,不提供异常安全保证才能成为一个选项。
作为一个一般性的规则,你应该提供实际可达到的最强力的保证。从异常安全的观点看,不抛出的函数(nothrow functions)是极好的,但是在 C++ 的 C 部分之外部不调用可能抛出异常的函数简直就是寸步难行。使用动态分配内存的任何东西(例如,所有的 STL 容器)如果不能找到足够的内存来满足一个请求,在典型情况下,它就会抛出一个 bad_alloc 异常。只要你能做到就提供不抛出保证,但是对于大多数函数,选择是在基本的保证和强力的保证之间的。
在 changeBackground 的情况下,提供差不多的强力保证并不困难。首先,我们将 PrettyMenu 的 bgImage 数据成员的类型从一个内建的 Image* 指针改变为 Item 13 中描述的智能资源管理指针中的一种。坦白地讲,在预防资源泄漏的基本原则上,这完全是一个好主意。它帮助我们提供强大的异常安全保证的事实进一步加强了这样的观点——使用对象(诸如智能指针)管理资源是良好设计的基础。在下面的代码中,我展示了 tr1::shared_ptr 的使用,因为当进行通常的拷贝时它的更符合直觉的行为使得它比 auto_ptr 更可取。
第二,我们重新排列 changeBackground 中的语句,以致于直到图像发生变化,才增加 imageChanges。这是一个很好的策略——直到某件事情真正发生了,再改变一个对象的状态来表示某事已经发生。
这就是修改之后的代码:
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; } |
注意这里不再需要手动删除旧的图像,因为在智能指针内部已经被处理了。此外,只有当新的图像被成功创建了删除行为才会发生。更准确地说,只有当 tr1::shared_ptr::reset 函数的参数("new Image(imgSrc)" 的结果)被成功创建了,这个函数才会被调用。只有在对 reset 的调用的内部才会使用 delete,所以如果这个函数从来不曾进入,delete 就从来不曾使用。同样请注意一个管理资源(动态分配的 Image)的对象(tr1::shared_ptr)的使用再次缩短了 changeBackground 的长度。
正如我所说的,这两处改动差不多有能力使 changeBackground 提供强力异常安全保证。美中不足的是什么呢?参数 imgSrc。如果 Image 的构造函数抛出一个异常,输入流(input stream)的读标记(read marker)可能已经被移动,而这样的移动就成为对程序的其它部分来说可见的一个状态的变化。直到 changeBackground 着手解决这个问题之前,它只能提供基本异常安全保证。
无论如何,让我们把它放在一边,并且依然假装 changeBackground 可以提供强力保证。(我相信你至少能用一种方法做到这一点,或许可以通过将它的参数从一个 istream 改变到包含图像数据的文件的文件名。)有一种通常的设计策略可以有代表性地产生强力保证,而且熟悉它是非常必要的。这个策略被称为 "copy and swap"。它的原理很简单。先做出一个你要改变的对象的拷贝,然后在这个拷贝上做出全部所需的改变。如果改变过程中的某些操作抛出了异常,最初的对象保持不变。在所有的改变完全成功之后,将被改变的对象和最初的对象在一个不会抛出异常的操作中进行交换。 这通常通过下面的方法实现:将每一个对象中的全部数据从“真正的”对象中放入到一个单独的实现对象中,然后将一个指向实现对象的指针交给真正对象。这通常被称为 "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 做成一个结构体,而不是类,因为通过让 pImpl 是 private 就可以确保 PrettyMenu 数据的封装。将 PMImpl 做成一个类虽然有些不那么方便,却没有增加什么好处。(这也会使有面向对象洁癖者走投无路。)如果你愿意,PMImpl 可以嵌套在 PrettyMenu 内部,像这样的打包问题与我们这里所关心的写异常安全的代码的问题没有什么关系。
copy-and-swap 策略是一种全面改变或丝毫不变一个对象的状态的极好的方法,但是,在通常情况下,它不能保证全部函数都是强力异常安全的。为了弄清原因,考虑一个 changeBackground 的抽象化身—— someFunc,它使用了 copy-and-swap,但是它包含了对另外两个函数(f1 和 f2)的调用:
void someFunc() { ... // make copy of local state f1(); f2(); ... // swap modified state into place } |
很明显,如果 f1 或 f2 低于强力异常安全,someFunc 就很难成为强力异常安全的。例如,假设 f1 仅提供基本保证。为了让 someFunc 提供强力保证,它必须写代码在调用 f1 之前测定整个程序的状态,并捕捉来自 f1 的所有异常,然后恢复到最初的状态。
即使 f1 和 f2 都是强力异常安全的,事情也好不到哪去。如果 f1 运行完成,程序的状态已经发生了毫无疑问的变化,所以如果随后 f2 抛出一个异常,即使 f2 没有改变任何东西,程序的状态也已经和调用 someFunc 时不同。
问题在于副作用。只要函数仅对局部状态起作用(例如,someFunc 仅仅影响调用它的那个对象的状态),它提供强力保证就相对容易。当函数的副作用影响了非局部数据,它就会困难得多。例如,如果调用 f1 的副作用是改变数据库,让 someFunc 成为强力异常安全就非常困难。一般情况下,没有办法撤销已经提交的数据库变化,其他数据库客户可能已经看见了数据库的新状态。
类似这样的问题会阻止你为函数提供强力保证,即使你希望去做。另一个问题是效率。copy-and-swap 的要点是这样一个想法:改变一个对象的数据的拷贝,然后在一个不会抛出异常的操作中将被改变的数据和原始数据进行交换。这就需要做出每一个要改变的对象的拷贝,这可能会用到你不能或不情愿动用的时间和空间。强力保证是非常值得的,当它可用时你应该提供它,除非在它不能 100% 可用的时候。
当它不可用时,你就必须提供基本保证。在实践中,你可能会发现你能为某些函数提供强力保证,但是效率和复杂度的成本使得它难以支持大量的其它函数。无论何时,只要你作出过一个提供强力保证的合理的成果,就没有人会因为你仅仅提供了基本保证而站在批评你的立场上。对于很多函数来说,基本保证是一个完全合理的选择。
如果你写了一个根本没有提供异常安全保证的函数,事情就不同了,因为在这一点上有罪推定是合情合理的,直到你证明自己是清白的。你应该写出异常安全的代码。除非你能做出有说服力的答辩。请再次考虑 someFunc 的实现,它调用了函数 f1 和 f2。假设 f2 根本没有提供异常安全保证,甚至没有基本保证。这就意味着如果 f2 发生一个异常,程序可能会在 f2 内部泄漏资源。这也意味着 f2 可能会恶化数据结构,例如,已排序数组可能不再排序,一个正在从一个数据结构传送到另一个数据结构去的对象可能丢失,等等。没有任何办法可以让 someFunc 能弥补这些问题。如果 someFunc 调用的函数不提供异常安全保证,someFunc 本身就不能提供任何保证。
请允许我回到怀孕的话题。一个女性或者怀孕或者没有。局部怀孕是绝不可能的。与此相似,一个软件或者是异常安全的或者不是。没有像一个局部异常安全的系统这样的东西。一个系统即使只有一个函数不是异常安全的,那么系统作为一个整体就不是异常安全的,因为调用那个函数可能发生泄漏资源和恶化数据结构。不幸的是,很多 C++ 的遗留代码在写的时候没有留意异常安全,所以现在的很多系统都不是异常安全的。它们混合了用非异常安全(exception-unsafe)的方式书写的代码。
没有理由让事情的这种状态永远持续下去。当书写新的代码或改变现存代码时,要仔细考虑如何使它异常安全。以使用对象管理资源开始。这样可以防止资源泄漏。接下来,决定三种异常安全保证中的哪一种是你实际上能够为你写的每一个函数提供的最强的保证,只有当你不调用遗留代码就别无选择的时候,才能满足于没有保证。既是为你的函数的客户也是为了将来的维护人员,文档化你的决定。一个函数的异常安全保证是它的接口的可见部分,所以你应该特意选择它,就像你特意选择一个函数接口的其它方面。
四十年前,到处都是 goto 的代码被尊为最佳实践。现在我们为书写结构化控制流程而奋斗。二十年前,全局可访问数据被尊为最佳实践。现在我们为封装数据而奋斗,十年以前,写函数时不必考虑异常的影响被尊为最佳实践。现在我们为写异常安全的代码而奋斗。
时光在流逝。我们生活着。我们学习着。
Things to Remember
·即使当异常被抛出时,异常安全的函数不会泄露资源,也不允许数据结构被恶化。这样的函数提供基本的,强力的,或者不抛出保证。
·强力保证经常可以通过 copy-and-swap 被实现,但是强力保证并非对所有函数都可用。
·一个函数通常能提供的保证不会强于他所调用的函数中最弱的保证。
###adv###
3:如何编写异常安全的代码
http://blog.csdn.net/wingfiring/article/details/660900
转:
【C++深入探索】Copy-and-swap idiom详解和实现安全自我赋值
任何管理某资源的类比如智能指针需要遵循一个规则(The Rule of Three):
如果你需要显式地声明一下三者中的一个:析构函数、拷贝构造函数或者是拷贝赋值操作符,那么你需要显式的声明所有这三者。
拷贝构造函数和析构函数实现起来比较容易,但是拷贝赋值操作符要复杂许多。
它是怎么实现的?我们需要避免那些误区?
那么Copy-and-swap就是完美的解决方案。而且可以很好地帮助拷贝赋值操作符达到两个目标:避免代码重复、提供强烈的异常安全保证。
1、 怎么工作
概念上讲,它是利用拷贝构造函数生成一个临时拷贝,然后使用swap函数将此拷贝对象与旧数据交换。然后临时对象被析构,旧数据消失。我们就拥有了新数据的拷贝。
为了使用copy-and-swap,我们需要拷贝构造函数、析构函数以及swap交换函数。
一个交换函数是一个non-throwing函数,用来交换某个类的两个对象,按成员交换。我们可能会试着使用std:swap,但是这不可行。因为std:swap使用自己的拷贝构造函数和拷贝赋值操作符。而我们的目的是定义自己的拷贝赋值操作符。
2、 目的
让我们看一个具体的实例。我们需要在一个类中管理一个动态数组。我们需要实现构造函数、拷贝赋值操作符、析构函数。
#include <algorithm> // std::copy #include <cstddef> // std::size_t class dumb_array { public: // (default) constructor dumb_array(std::size_t size = 0) : mSize(size), mArray(mSize ? new int[mSize]() : 0) {} // copy-constructor dumb_array(const dumb_array& other) : mSize(other.mSize), mArray(mSize ? new int[mSize] : 0), { // note that this is non-throwing, because of the data // types being used; more attention to detail with regards // to exceptions must be given in a more general case, however std::copy(other.mArray, other.mArray + mSize, mArray); } // destructor ~dumb_array() { delete [] mArray; } private: std::size_t mSize; int* mArray; };
这个类几乎可以说是成功的实现了管理动态类的功能,但是还需要opeator=才能正常工作。
下面是一个不怎么好的实现:
// the hard part dumb_array& operator=(const dumb_array& other) { if (this != &other) // (1) { // get rid of the old data... delete [] mArray; // (2) mArray = 0; // (2) *(see footnote for rationale) // ...and put in the new mSize = other.mSize; // (3) mArray = mSize ? new int[mSize] : 0; // (3) std::copy(other.mArray, other.mArray + mSize, mArray); // (3) } return *this; }
上述代码有三个问题,分别是括号所注明的。
(1)需要进行自我赋值判别。
这个判别有两个目的:是一个阻止冗余代码的一个简单的方法;可以防止出现bug(删除数组接着又进行复制操作)。在其他时候不会有什么问题,只是使得程序变慢了。自我赋值在程序中比较少见,所以大部分情况下这个判别是多余的。这样,如果没有这个判别也能够正常工作就更好了。
(2)只提供了基本异常安全保证。
如果new int[mSize]失败,那么*this就被修改了(数组大小是错误的,数组也丢失了)。为了提供强烈保证,需要这样做:
dumb_array& operator=(const dumb_array& other) { if (this != &pOther) // (1) { // get the new data ready before we replace the old std::size_t newSize = other.mSize; int* newArray = newSize ? new int[newSize]() : 0; // (3) std::copy(other.mArray, other.mArray + newSize, newArray); // (3) // replace the old data (all are non-throwing) delete [] mArray; mSize = newSize; mArray = newArray; } return *this; }
代码膨胀了!这就导致了另外一个问题:
(3)代码冗余。
核心代码只有两行即分配空间和拷贝。如果要实现比较复杂的资源管理,那么代码的膨胀将会导致非常严重的问题。
3、一个成功的解决方案
就像前面所提到的,copy-and-swap可以解决所有这些问题。但是现在,我们还需要完成另外一件事:swap函数。规则“The rule of three”指明了拷贝构造函数、赋值操作符以及析构函数的存在。其实它应该被称作是“The Big And Half”:任何时候你的类要管理一个资源,提供swap函数是有必要的。
我们需要向我们的类添加swap函数,看以下代码:
class dumb_array { public: // ... friend void swap(dumb_array& first, dumb_array& second) // nothrow { // enable ADL (not necessary in our case, but good practice) using std::swap; // by swapping the members of two classes, // the two classes are effectively swapped swap(first.mSize, second.mSize); swap(first.mArray, second.mArray); } // ... };
现在我们不仅可以交换dumb_array,而且交换是很有效率的进行:它只是交换指针和数组大小,而不是重新分配空间和拷贝整个数组。
这样,我们可以如下实现拷贝赋值操作符:
dumb_array& operator=(dumb_array other) // (1) { swap(*this, other); // (2) return *this; }
就是这样!以上提到的三个问题全部获得解决。
4、为什么可以正常工作
我们注意到一个很重要的细节:参数是按值传递的。
某些人可能会轻易地这样做(实际上,很多失败的实现都是这么做的):
dumb_array& operator=(const dumb_array& other) { dumb_array temp(other); swap(*this, temp); return *this; }
这样做我们会失去一个重要的优化机会(参考Want Speed? Pass by Value)。而在C++11中,它备受争议。
通常,我们最好遵循比较有用的规则是:不要拷贝函数参数。你应该按值传递参数,让编译器来完成拷贝工作。
这种管理资源的方式解决了代码冗余的问题,我们可以用拷贝构造函数完成拷贝功能,而不用按位拷贝。拷贝功能完成后,我们就可以准备交换了。
注意到,上面一旦进入函数体,所有新数据都已经被分配、拷贝,可以使用了。这就提供了强烈的异常安全保证:如果拷贝失败,我们不会进入到函数体内,那么this指针所指向的内容也不会被改变。(在前面我们为了实施强烈保证所做的事情,现在编译器为我们做了)。
swap函数时non-throwing的。我们把旧数据和新数据交换,安全地改变我们的状态,旧数据被放进了临时对象里。这样当函数退出时候,旧数据被自动释放。
因为copy-and-swap没有代码冗余,我们不会在这个而操作符里面引入bug。我们也避免了自我赋值检测。
参考资料:
http://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom
转自:http://blog.csdn.net/xiajun07061225/article/details/7926722