码农的世界C++ の 乐园C++

详解C++的 copy-and-swap 技巧

2018-07-28  本文已影响0人  CCCCCCode

我们先从异常安全(exception safety)谈起,要想写健壮的C++代码,异常安全是十分重要的。具有异常安全的函数提供了三种安全等级:

如果一个函数不能提供上述保证之一,则不具备异常安全性。下面,我们通过一个例子,讲解如何通过 copy-and-swap 技巧来达到异常安全的强烈保证。

假设我们需要在一个类中管理在一个类中管理一个动态数组,既然我们要自己管理内存,那我们就应该遵守三五原则,我们先实现拷贝构造函数和析构函数。

#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] : nullptr)
    {}

    // copy-constructor
    dumb_array(const dumb_array& other) :
        mSize(other.mSize),
        mArray(mSize ? new int[mSize] : nullptr)
    {
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }

    // destructor
    ~dumb_array()
    {
        delete[] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};

下面实现拷贝赋值函数(这是我们重点讨论的对象),我们在掌握 copy-and-swap 技巧之前,一般会这样实现:

dumb_array& operator=(const dumb_array& other)
    {
        if (this != &other) // Problem (1)
        {
            delete[] mArray; // Problem (2)
            mArray = 0;
            mSize = other.mSize;
            mArray = mSize ? new int[mSize] : 0;
            std::copy(other.mArray, other.mArray + mSize, mArray);
        }
        return *this;
    }

这种实现方式存在以下缺陷:

dumb_array& operator=(const dumb_array& other)
{
    if (this != &other)
    {
        // get the new data ready before we replace the old
        std::size_t newSize = other.mSize;
        int* newArray = newSize ? new int[newSize]() : 0; // Problem (3)
        std::copy(other.mArray, other.mArray + newSize, newArray);

        // replace the old data (all are non-throwing)
        delete[] mArray;
        mSize = newSize;
        mArray = newArray;
    }

    return *this;
}

但这样就会导致代码膨胀,于是导致了另一个问题:

就像前面所提到的,copy-and-swap 可以解决以上所有这些问题。但是现在,我们还需要完成另外一件事:swap函数。对,就是 copy-and-swap 中的那个swap。其实,任何时候你的类要管理一个资源,提供swap函数是有必要的。我们需要向我们的类添加swap函数,看以下代码:

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);
}

此swap函数只交换指针和数组大小,而不是重新分配空间和拷贝整个数组,故效率很高。

至此,我们可以使用 copy-and-swap 技巧实现拷贝赋值操作符了:

dumb_array& operator=(dumb_array other) 
{
    swap(*this, other); 
    return *this;
}

是的,你没有看错,就是这么简洁优雅。下面让我们仔细看看为什么它能更好地完成任务:

在C++11引入move语义后,根据三五法则,我们一般要实现移动构造函数和移动赋值运算符。copy-and-swap 技巧 也可以用到 move 语义上,变成 move-and-swap 技巧,完成对移动赋值运算符的改进。

实际上,我们比那个不需要多写代码,copy-and-swap 技巧 和 move-and-swap 技巧是共享同一个函数的。对,还是这个:

dumb_array& operator=(dumb_array other) 
{
    swap(*this, other); 
    return *this;
}

对于C++ 11,编译器会依据参数是左值还是右值在拷贝构造函数和移动构造函数间进行选择:

我们也可以称呼它为“统一赋值操作符”,因为它合“拷贝赋值”、“移动赋值”为一体了。

参考文献

  1. 书籍:C++ Primer(第五版)
  2. 文章:Copy-and-swap idiom详解和实现安全自我赋值
上一篇 下一篇

猜你喜欢

热点阅读