问题

这个成语是什么,什么时候使用?它解决了哪些问题?使用C ++ 11时惯用语是否改变?

虽然在很多地方都提到过,我们没有任何奇异的"什么是"问题和答案,所以这里是.以下是之前提到的地方的部分列表:



解决方法

Overview

Why do we need the copy-and-swap idiom?

管理资源的任何类( wrapper ,例如智能指针)需要实现代码重复,并提供强异常保证.

How does it work?

概念上,它的工作原理是使用复制构造函数创建数据的本地副本,然后使用 swap 函数获取复制的数据,使用新数据交换旧数据.然后临时副本会破坏,用旧数据.我们将留下一份新数据的副本.

为了使用复制和交换惯例,我们需要三个东西:一个工作的复制构造函数,一个工作的析构函数(都是任何包装的基础,所以应该是完整的),和一个 swap 函数.

交换函数是一个非投掷函数,它交换类的两个对象,成员的成员.我们可能会使用 std :: swap 而不是提供自己的,但这是不可能的; std :: swap 在其实现中使用了拷贝构造函数和拷贝赋值运算符,我们最终会尝试自己定义赋值运算符!

(不仅如此,但对 swap 的非限定调用将使用我们的自定义交换运算符,跳过不必要的构造和破坏我们的类 std :: swap 包含.)


An in-depth explanation

The goal

让我们考虑一个具体的例子.我们想在一个无用的类中管理一个动态数组.我们从一个工作的构造函数,复制构造函数和析构函数开始:

#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),
    {
        // 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;
};

这个类几乎成功地管理数组,但它需要 operator = 才能正常工作.

A failed solution

这里是一个朴素的实现可能看起来:

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = nullptr; // (2) *(see footnote for rationale)

        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : nullptr; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }

    return *this;
}

我们说完了;这现在管理一个数组,没有泄漏.但是,它遇到了三个问题,在代码中按(n)的顺序标记.

  1. The first is the self-assignment test. This check serves two purposes: it's an easy way to prevent us from running needless code on self-assignment, and it protects us from subtle bugs (such as deleting the array only to try and copy it). But in all other cases it merely serves to slow the program down, and act as noise in the code; self-assignment rarely occurs, so most of the time this check is a waste. It would be better if the operator could work properly without it.

  2. The second is that it only provides a basic exception guarantee. If new int[mSize] fails, *this will have been modified. (Namely, the size is wrong and the data is gone!) For a strong exception guarantee, it would need to be something akin to:

    dumb_array& operator=(const dumb_array& other)
    {
        if (this != &other) // (1)
        {
            // get the new data ready before we replace the old
            std::size_t newSize = other.mSize;
            int* newArray = newSize ? new int[newSize]() : nullptr; // (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. The code has expanded! Which leads us to the third problem: code duplication. Our assignment operator effectively duplicates all the code we've already written elsewhere, and that's a terrible thing.

在我们的例子中,它的核心只有两行(分配和副本),但是使用更复杂的资源,这个代码膨胀可能是一个麻烦.我们应该努力不再重复自己.

(有人可能会想:如果需要这么多的代码来正确地管理一个资源,那么如果我的类管理多个?那么这可能是一个有效的问题,并且确实需要非平凡的 try / catch 子句,这是一个非问题,因为一个类应该管理 一个资源 !)

A successful solution

如前所述,复制和交换惯用法将修复所有这些问题.但现在,我们有所有的要求,除了一个: 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 objects,
        // the two objects are effectively swapped
        swap(first.mSize, second.mSize);
        swap(first.mArray, second.mArray);
    }

    // ...
};

(这里是为什么 public friend swap .)现在,我们不仅可以交换我们的 dumb_array ,但是一般来说swap可以更高效;它只是交换指针和大小,而不是分配和复制整个数组.除了在功能和效率上的这个奖金,我们现在准备实现复制和交换惯用语.

不用多说,我们的赋值运算符是:

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)

    return *this;
}

就是这样!只要一动不动,所有三个问题就一起优雅地解决了.

Why does it work?

我们首先注意到一个重要的选择:参数参数取自 by-value .虽然人们可以很容易地做下面的事情(事实上,很多幼稚的实现):

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);

    return *this;
}

我们失去了重要的最佳化商机.不仅如此,但这种选择在C ++ 11中是至关重要的,稍后讨论. (一般来说,一个非常有用的指南如下:如果你要在函数中创建一个副本,让编译器在参数列表中执行)‡)

无论如何,这种获取我们的资源的方法是消除代码重复的关键:我们使用来自复制构造函数的代码来制作副本,而不需要重复任何位.现在复制完成了,我们准备交换了.

请注意,在输入所有新数据已分配,复制并准备使用的函数时.这是给我们一个强大的异常保证的免费:我们甚至不会输入函数,如果构造的副本失败,因此不可能改变 * this 的状态. (我们之前手动执行了强大的异常保证,编译器为我们现在做了;如何实际.)

在这一点上,我们是自由的,因为 swap 是不抛出.我们使用复制的数据交换我们当前的数据,安全地更改我们的状态,并将旧数据放入临时数据.然后当函数返回时释放旧数据. (其中参数的作用域结束,它的析构函数被调用.)

因为惯用语不重复任何代码,我们不能在操作符中引入错误.注意,这意味着我们不需要一个自我赋值检查,允许单一的统一实现 operator = . (此外,我们不再对非自我分配造成性能损失.)

这就是复制和交换惯用语.

What about C++11?

下一版本的C ++,C ++ 11,对我们如何管理资源有一个非常重要的变化:三分法现在是四元规则(一半).为什么?因为我们不仅需要能够复制 - 构造我们的资源,我们需要移动 - 构建它.

幸运的是,对我们来说,这很容易:

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other)
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

这里发生了什么?回忆移动构建的目标:从类的另一个实例获取资源,将其保留在可分配和可破坏的状态.

所以我们所做的是简单的:通过默认构造函数初始化(C ++ 11特性),然后用 other 交换;我们知道我们的类的一个默认构造的实例可以被安全地分配和销毁,所以我们知道 other 在交换后也能做同样的事情.

(请注意,一些编译器不支持构造函数委托;在这种情况下,我们必须手动默认构造类,这是一个不幸的但幸运的小任务.)

Why does that work?

这是我们需要对我们的类做的唯一的改变,为什么它工作?记住我们使参数成为值而不是参考的非常重要的决定:

dumb_array& operator=(dumb_array other); // (1)

现在,如果其他正在使用右值初始化,将是移动构造.完善.以同样的方式C ++ 03让我们通过采用参数by-value重用我们的复制构造函数,C ++ 11将自动地在适当的时候选择move-constructor. (当然,如在先前链接的文章中提到的,值的复制/移动可以完全省略.)

因此结束了复制和交换惯用语.


Footnotes

*为什么我们将 mArray 设置为null?因为如果操作符中的任何其他代码引发,可以调用 dumb_array 的析构函数;如果发生这种情况,而没有将其设置为null,我们尝试删除已经删除的内存!我们通过将其设置为null来避免这种情况,因为删除null是无操作.

†还有其他声明,我们应该专注于 std :: swap 为我们的类型,提供一个交换旁边的自由功能交换等.但是这一切都是不必要的:任何正确使用 swap 将通过非限定调用,我们的函数将通过 ADL .一个功能会做.

‡原因很简单:一旦你有资源给自己,你可以在任何需要的地方交换和/或移动它(C ++ 11).通过在参数列表中制作副本,您可以最大化优化.




相关问题推荐