分类: C/C++2012-08-30 21:40 2017人阅读 评论(2) 收藏 举报

任何管理某资源的类比如智能指针需要遵循一个规则(The Rule of Three):

如果你需要显式地声明一下三者中的一个:析构函数、拷贝构造函数或者是拷贝赋值操作符,那么你需要显式的声明所有这三者。

拷贝构造函数和析构函数实现起来比较容易,但是拷贝赋值操作符要复杂许多。

它是怎么实现的?我们需要避免那些误区?

那么Copy-and-swap就是完美的解决方案。而且可以很好地帮助拷贝赋值操作符达到两个目标:避免代码重复、提供强烈的异常安全保证。

1、  怎么工作

概念上讲,它是利用拷贝构造函数生成一个临时拷贝,然后使用swap函数将此拷贝对象与旧数据交换。然后临时对象被析构,旧数据消失。我们就拥有了新数据的拷贝。

为了使用copy-and-swap,我们需要拷贝构造函数、析构函数以及swap交换函数。

一个交换函数是一个non-throwing函数,用来交换某个类的两个对象,按成员交换。我们可能会试着使用std:swap,但是这不可行。因为std:swap使用自己的拷贝构造函数和拷贝赋值操作符。而我们的目的是定义自己的拷贝赋值操作符。

2、  目的

让我们看一个具体的实例。我们需要在一个类中管理一个动态数组。我们需要实现构造函数、拷贝赋值操作符、析构函数。

  1. #include <algorithm> // std::copy
  2. #include <cstddef> // std::size_t
  3. class dumb_array
  4. {
  5. public:
  6. // (default) constructor
  7. dumb_array(std::size_t size = 0) :
  8. mSize(size),
  9. mArray(mSize ? new int[mSize]() : 0)
  10. {}
  11. // copy-constructor
  12. dumb_array(const dumb_array& other) :
  13. mSize(other.mSize),
  14. mArray(mSize ? new int[mSize] : 0),
  15. {
  16. // note that this is non-throwing, because of the data
  17. // types being used; more attention to detail with regards
  18. // to exceptions must be given in a more general case, however
  19. std::copy(other.mArray, other.mArray + mSize, mArray);
  20. }
  21. // destructor
  22. ~dumb_array()
  23. {
  24. delete [] mArray;
  25. }
  26. private:
  27. std::size_t mSize;
  28. int* mArray;
  29. };

这个类几乎可以说是成功的实现了管理动态类的功能,但是还需要opeator=才能正常工作。

下面是一个不怎么好的实现:

  1. // the hard part
  2. dumb_array& operator=(const dumb_array& other)
  3. {
  4. if (this != &other) // (1)
  5. {
  6. // get rid of the old data...
  7. delete [] mArray; // (2)
  8. mArray = 0; // (2) *(see footnote for rationale)
  9. // ...and put in the new
  10. mSize = other.mSize; // (3)
  11. mArray = mSize ? new int[mSize] : 0; // (3)
  12. std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
  13. }
  14. return *this;
  15. }

上述代码有三个问题,分别是括号所注明的。

(1)需要进行自我赋值判别。

这个判别有两个目的:是一个阻止冗余代码的一个简单的方法;可以防止出现bug(删除数组接着又进行复制操作)。在其他时候不会有什么问题,只是使得程序变慢了。自我赋值在程序中比较少见,所以大部分情况下这个判别是多余的。这样,如果没有这个判别也能够正常工作就更好了。

(2)只提供了基本异常安全保证。

如果new int[mSize]失败,那么*this就被修改了(数组大小是错误的,数组也丢失了)。为了提供强烈保证,需要这样做:

  1. dumb_array& operator=(const dumb_array& other)
  2. {
  3. if (this != &pOther) // (1)
  4. {
  5. // get the new data ready before we replace the old
  6. std::size_t newSize = other.mSize;
  7. int* newArray = newSize ? new int[newSize]() : 0; // (3)
  8. std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
  9. // replace the old data (all are non-throwing)
  10. delete [] mArray;
  11. mSize = newSize;
  12. mArray = newArray;
  13. }
  14. return *this;
  15. }

代码膨胀了!这就导致了另外一个问题:

(3)代码冗余。
核心代码只有两行即分配空间和拷贝。如果要实现比较复杂的资源管理,那么代码的膨胀将会导致非常严重的问题。

3、一个成功的解决方案


就像前面所提到的,copy-and-swap可以解决所有这些问题。但是现在,我们还需要完成另外一件事:swap函数。规则“The rule of three”指明了拷贝构造函数、赋值操作符以及析构函数的存在。其实它应该被称作是“The Big And Half”:任何时候你的类要管理一个资源,提供swap函数是有必要的。

我们需要向我们的类添加swap函数,看以下代码:

  1. class dumb_array
  2. {
  3. public:
  4. // ...
  5. friend void swap(dumb_array& first, dumb_array& second) // nothrow
  6. {
  7. // enable ADL (not necessary in our case, but good practice)
  8. using std::swap;
  9. // by swapping the members of two classes,
  10. // the two classes are effectively swapped
  11. swap(first.mSize, second.mSize);
  12. swap(first.mArray, second.mArray);
  13. }
  14. // ...
  15. };

现在我们不仅可以交换dumb_array,而且交换是很有效率的进行:它只是交换指针和数组大小,而不是重新分配空间和拷贝整个数组。
这样,我们可以如下实现拷贝赋值操作符:

  1. dumb_array& operator=(dumb_array other) // (1)
  2. {
  3. swap(*this, other); // (2)
  4. return *this;
  5. }

就是这样!以上提到的三个问题全部获得解决。

4、为什么可以正常工作

我们注意到一个很重要的细节:参数是按值传递的。

某些人可能会轻易地这样做(实际上,很多失败的实现都是这么做的):

  1. dumb_array& operator=(const dumb_array& other)
  2. {
  3. dumb_array temp(other);
  4. swap(*this, temp);
  5. return *this;
  6. }

这样做我们会失去一个重要的优化机会(参考Want Speed? Pass by Value)。而在C++11中,它备受争议。
通常,我们最好遵循比较有用的规则是:不要拷贝函数参数。你应该按值传递参数,让编译器来完成拷贝工作。

这种管理资源的方式解决了代码冗余的问题,我们可以用拷贝构造函数完成拷贝功能,而不用按位拷贝。拷贝功能完成后,我们就可以准备交换了。

注意到,上面一旦进入函数体,所有新数据都已经被分配、拷贝,可以使用了。这就提供了强烈的异常安全保证:如果拷贝失败,我们不会进入到函数体内,那么this指针所指向的内容也不会被改变。(在前面我们为了实施强烈保证所做的事情,现在编译器为我们做了)。

swap函数时non-throwing的。我们把旧数据和新数据交换,安全地改变我们的状态,旧数据被放进了临时对象里。这样当函数退出时候,旧数据被自动释放。

因为copy-and-swap没有代码冗余,我们不会在这个而操作符里面引入bug。我们也避免了自我赋值检测。

【C++深入探索】Copy-and-swap idiom详解和实现安全自我赋值的更多相关文章

  1. objective-C: nonatomic retain copy assgin 等属性详解

    http://my.oschina.net/u/728866/blog/90798 property,可以提供的功能有:提供成员变量的访问方法的声明.控制成员变量的访问权限.控制多线程时成员变量的访问 ...

  2. 探索c#之一致性Hash详解

    阅读目录: 使用场景 算法原理 虚拟节点 代码示例 使用场景 以Redis为例,当系统需要缓存的内容超过单机内存大小时,例如要缓存100G数据,单机内存仅有16G时.这时候就需要考虑进行缓存数据分片, ...

  3. 探索C++的秘密之详解extern "C",这就是为什么很多.lib被我们正确调用确总是无法解析的。

    (转载,绝对的有用) lib被我们正确调用确总是无法解析.这是C++编译和C编译的区别 时常在cpp的代码之中看到这样的代码: #ifdef __cplusplus extern "C&qu ...

  4. 【转】探索C++的秘密之详解extern

    本文转自:http://i.cnblogs.com/EditPosts.aspx?opt=1 时常在cpp的代码之中看到这样的代码: #ifdef __cplusplus extern "C ...

  5. (转)探索C++的秘密之详解extern "C",这就是为什么很多.lib被我们正确调用确总是无法解析的。

    (转载,绝对的有用) lib被我们正确调用确总是无法解析.这是C++编译和C编译的区别 时常在cpp的代码之中看到这样的代码: #ifdef __cplusplus extern "C&qu ...

  6. Webpack探索【5】--- plugins详解

    本文主要讲plugins相关内容. https://gitbook.cn/gitchat/column/59e065f64f7fbe555e479204/topic/59e96d87a35cf44e1 ...

  7. Webpack探索【3】--- loader详解

    本文主要说明Webpack的loader相关内容.

  8. Webpack探索【12】--- externals详解

    本文主要讲externals相关内容. https://segmentfault.com/a/1190000012113011

  9. 探索C++的秘密之详解extern

    转载:http://developer.51cto.com/art/200704/46843.htm C和C++对函数的处理方式是不同的.extern "C"是使C++能够调用C写 ...

随机推荐

  1. oracle解析xml完成版

    SELECT * FROM XMLTABLE('$B/DEAL_BASIC/USER_DEAL_INFO' PASSING XMLTYPE('<?xml version="1.0&qu ...

  2. 对于IEnumerable的一点理解

    IEnumerable和IEnumerable<T>接口在.NET中是非常重要的接口,它允许开发人员定义foreach语句功能的实现并支持非泛型方法的简单的迭代,IEnumerable和I ...

  3. 一款好看+极简到不行的HTML5音乐播放器-skPlayer

    Demo: github skPlayer在线预览 预览: 单曲循环模式预览: 使用方法: 方式1:NPM npm install skplayer 方式2:引入文件 引入css文件: <lin ...

  4. oracle常用SQL总结

    这里我们介绍的是 40+ 个非常有用的 Oracle 查询语句,主要涵盖了日期操作,获取服务器信息,获取执行状态,计算数据库大小等等方面的查询.这些是所有 Oracle 开发者都必备的技能,所以快快收 ...

  5. memcache锁

    锁的使用,一般情况是针对并发或者我们希望程序(crontab的job)串行处理,我们加锁的办法有很多,像文件锁,数据库锁,或者memcache锁,这里关注一下memcache锁,针对memcache锁 ...

  6. python使用PIL压缩图片

    import Image import os import os.path import sys path = sys.argv[1] small_path = (path[:-1] if path[ ...

  7. hdu 4548 第六周H题(美素数)

    第六周H题 - 数论,晒素数 Time Limit:1000MS     Memory Limit:32768KB     64bit IO Format:%I64d & %I64u   De ...

  8. d037: 鸡兔同笼

    内容: 鸡兔同笼中头的总数,脚的总数,求鸡兔各多少只 输入说明: 一行两个整数分别表示头.脚总数(保证有解,当然有可能解为0) 输出说明: 一行两个整数,分别表示鸡.兔的只数 输入样例:   20 4 ...

  9. C# 代码中 计算某个函数 或WebService 请求花费时间

    /// 计算请求所花费的时间 System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch(); watch.Start( ...

  10. asp.net 后台使用js弹窗失效问题

    1.这些事件输出来前后都变成JS代码了,看到到这样的代码的了.会变成<script>alert('合同号XXX已存在')</script>首先后台调试一下看看Page.Clie ...