• 【C++深入探索】Copy-and-swap idiom详解和实现安全自我赋值


     

    分类: C/C++

    任何管理某资源的类比如智能指针需要遵循一个规则(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.   
    4. class dumb_array  
    5. {  
    6. public:  
    7.     // (default) constructor  
    8.     dumb_array(std::size_t size = 0) :  
    9.       mSize(size),  
    10.           mArray(mSize ? new int[mSize]() : 0)  
    11.       {}  
    12.   
    13.       // copy-constructor  
    14.       dumb_array(const dumb_array& other) :  
    15.       mSize(other.mSize),  
    16.           mArray(mSize ? new int[mSize] : 0),  
    17.       {  
    18.           // note that this is non-throwing, because of the data  
    19.           // types being used; more attention to detail with regards  
    20.           // to exceptions must be given in a more general case, however  
    21.           std::copy(other.mArray, other.mArray + mSize, mArray);  
    22.       }  
    23.   
    24.       // destructor  
    25.       ~dumb_array()  
    26.       {  
    27.           delete [] mArray;  
    28.       }  
    29.   
    30. private:  
    31.     std::size_t mSize;  
    32.     int* mArray;  
    33. };  
    这个类几乎可以说是成功的实现了管理动态类的功能,但是还需要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.   
    10.         // ...and put in the new  
    11.         mSize = other.mSize; // (3)  
    12.         mArray = mSize ? new int[mSize] : 0; // (3)  
    13.         std::copy(other.mArray, other.mArray + mSize, mArray); // (3)  
    14.     }  
    15.   
    16.     return *this;  
    17. }   
    上述代码有三个问题,分别是括号所注明的。

    (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.   
    10.         // replace the old data (all are non-throwing)  
    11.         delete [] mArray;  
    12.         mSize = newSize;  
    13.         mArray = newArray;  
    14.     }  
    15.   
    16.     return *this;  
    17. }   
    代码膨胀了!这就导致了另外一个问题:

    (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.   
    6.     friend void swap(dumb_array& first, dumb_array& second) // nothrow  
    7.     {  
    8.         // enable ADL (not necessary in our case, but good practice)  
    9.         using std::swap;   
    10.   
    11.         // by swapping the members of two classes,  
    12.         // the two classes are effectively swapped  
    13.         swap(first.mSize, second.mSize);   
    14.         swap(first.mArray, second.mArray);  
    15.     }  
    16.   
    17.     // ...  
    18. };  
    现在我们不仅可以交换dumb_array,而且交换是很有效率的进行:它只是交换指针和数组大小,而不是重新分配空间和拷贝整个数组。
    这样,我们可以如下实现拷贝赋值操作符:
    1. dumb_array& operator=(dumb_array other) // (1)  
    2. {  
    3.     swap(*this, other); // (2)  
    4.   
    5.     return *this;  
    6. }   
    就是这样!以上提到的三个问题全部获得解决。

    4、为什么可以正常工作

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

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

    1. dumb_array& operator=(const dumb_array& other)  
    2. {  
    3.     dumb_array temp(other);  
    4.     swap(*this, temp);  
    5.   
    6.     return *this;  
    7. }  
    这样做我们会失去一个重要的优化机会(参考Want Speed? Pass by Value)。而在C++11中,它备受争议。
    通常,我们最好遵循比较有用的规则是:不要拷贝函数参数。你应该按值传递参数,让编译器来完成拷贝工作。


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

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

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

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

  • 相关阅读:
    使用Java实现对MySql数据库的导入与导出
    【转】揭开J2EE集群的神秘面纱
    Memcached深度分析
    HSQL入门及使用指南
    系统架构基础篇(高性能基础建设说明与选型条件)
    架构之美 摘抄
    JMS规范及相关实现
    spring3中使用@value注解获取属性值
    Thread Dump 分析综述
    什么中间件及中间件服务器?
  • 原文地址:https://www.cnblogs.com/lvdongjie/p/4513846.html
Copyright © 2020-2023  润新知