其他分享
首页 > 其他分享> > c – 什么是复制和交换习语?

c – 什么是复制和交换习语?

作者:互联网

这个成语是什么,什么时候应该使用?它解决了哪些问题?当使用C 11时,习语会改变吗?

虽然在许多地方已经提到过,但我们没有任何单一的“它是什么”问题和答案,所以在这里.以下是前面提到的地方的部分列表:

> What are your favorite C++ Coding Style idioms: Copy-swap
> Copy constructor and = operator overload in C++: is a common function possible?
> What is copy elision and how it optimizes copy-and-swap idiom
> C++: dynamically allocating an array of objects?

解决方法:

概观

为什么我们需要复制和交换习惯用法?

任何管理资源的类(包装器,如智能指针)都需要实现The Big Three.虽然复制构造函数和析构函数的目标和实现很简单,但复制赋值运算符可能是最细微和最困难的.应该怎么做?需要避免哪些陷阱?

复制和交换习语是解决方案,并且优雅地帮助分配运算符实现两件事:避免code duplication,并提供strong exception guarantee.

它是如何工作的?

Conceptually,它通过使用复制构造函数的功能来创建数据的本地副本,然后使用交换函数获取复制的数据,使用新数据交换旧数据.然后临时拷贝破坏,用它来获取旧数据.我们留下了新数据的副本.

为了使用复制和交换习惯用法,我们需要三件事:一个工作的复制构造函数,一个工作的析构函数(两者都是任何包装器的基础,所以应该是完整的),以及一个交换函数.

交换函数是一种非抛出函数,它交换类的两个对象,成员的成员.我们可能想要使用std :: swap而不是提供我们自己的,但这是不可能的; std :: swap在其实现中使用了copy-constructor和copy-assignment运算符,我们最终会尝试根据自身定义赋值运算符!

(不仅如此,但对swap的非限定调用将使用我们的自定义交换运算符,跳过std :: 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),
    {
        // 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 =才能正常工作.

失败的解决方案

这是一个天真的实现可能看起来如何:

// 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).

>首先是自我指派测试.这个检查有两个目的:它是一种简单的方法来阻止我们在自我分配上运行不必要的代码,它可以保护我们免受微妙的错误(例如删除数组只是为了尝试复制它).但在所有其他情况下,它只会减慢程序的速度,并在代码中充当噪声;自我指派很少发生,因此大多数时候这种检查是浪费.如果没有它,运算符可以正常工作会更好.
>第二是它只提供基本的异常保证.如果new int [mSize]失败,*这将被修改. (即,大小错误,数据不见了!)对于强大的异常保证,它需要类似于:

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

>代码已扩展!这引出了第三个问题:代码重复.我们的赋值运算符有效地复制了我们已经在其他地方写过的所有代码,这是一件非常糟糕的事情.

在我们的例子中,它的核心只有两行(分配和副本),但是由于资源更复杂,这个代码膨胀可能会非常麻烦.我们应该努力永不重复.

(有人可能会想:如果需要这么多代码来正确管理一个资源,那么如果我的类管理不止一个怎么办?虽然这似乎是一个有效的问题,实际上它需要非平凡的try / catch子句,这是一个非问题.那是因为一个班级应该管理one resource only!)

成功的解决方案

如上所述,复制和交换习惯用法将解决所有这些问题.但是现在,我们有除了一个以外的所有要求:交换功能.虽然The Rule of Three成功地要求我们的复制构造函数,赋值运算符和析构函数的存在,它应该被称为“三巨头半”:任何时候你的类管理资源,提供交换也是有意义的功能.

我们需要在我们的类中添加交换功能,我们这样做如下†:

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

    // ...
};

(Here解释为什么公众朋友互换.)现在我们不仅可以交换我们的dumb_array,而且交换一般可以更有效率;它只是交换指针和大小,而不是分配和复制整个数组.除了功能和效率方面的这一奖励外,我们现在已准备好实施复制和交换习惯用法.

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

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

    return *this;
}

就是这样!一举一动,所有三个问题都得到了优雅的解决.

它为什么有效?

我们首先注意到一个重要的选择:参数参数是按值进行的.虽然人们可以轻松地执行以下操作(事实上,许多简单的习惯实现):

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

    return *this;
}

我们失去了important optimization opportunity.不仅如此,这个选择在C 11中至关重要,后面将对此进行讨论. (一般来说,一个非常有用的指导如下:如果你要在函数中复制一些东西,让编译器在参数列表中执行.‡)

无论哪种方式,这种获取资源的方法是消除代码重复的关键:我们可以使用copy-constructor中的代码来制作副本,而不需要重复任何一点.现在副本已经完成,我们已准备好进行交换.

注意,在进入该功能时,已经分配,​​复制并准备好使用所有新数据.这就是免费提供强有力的异常保证:如果复制的构造失败,我们甚至不会进入函数,因此不可能改变* this的状态. (我们之前手动完成了强大的异常保证,编译器现在正在为我们做;多么善良.)

在这一点上,我们是无家可归的,因为交换是非投掷的.我们将当前数据与复制的数据交换,安全地改变我们的状态,并将旧数据放入临时数据中.然后在函数返回时释放旧数据. (在参数的作用域结束并调用其析构函数的位置.)

因为习惯用法不重复代码,所以我们不能在运算符中引入错误.请注意,这意味着我们不需要进行自我分配检查,从而允许单个统一实现operator =. (此外,我们不再对非自我分配造成性能损失.)

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

C 11怎么样?

C,C 11的下一个版本对我们管理资源的方式做了一个非常重要的改变:三个规则现在是四个规则(一半).为什么?因为我们不仅需要能够复制 – 构建我们的资源,we need to move-construct it as well.

幸运的是,这很容易:

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特性),然后与其他人交换;我们知道我们类的默认构造实例可以安全地分配和销毁,所以我们知道其他人在交换后也可以这样做.

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

为什么这样做?

这是我们需要对我们班级做出的唯一改变,那为什么它会起作用?请记住我们为使参数成为值而非参考而做出的非常重要的决定:

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

现在,如果使用rvalue初始化其他,则它将被移动构造.完善.以同样的方式,C 03让我们通过使用参数by-value重用我们的拷贝构造函数,C 11也会在适当的时候自动选择move-constructor. (当然,正如先前链接的文章中所提到的,可以简单地完全删除复制/移动值.)

因此,复制和交换习语就此结束.

脚注

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

†还有其他声称我们应该专门为我们的类型std :: swap,提供一个类内交换和自由函数交换等等.但这都是不必要的:任何正确使用swap都将通过不合格调用,我们的功能将在ADL找到.一个功能就可以了.

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

标签:c,assignment-operator,copy-constructor,c-faq,copy-and-swap
来源: https://codeday.me/bug/20190910/1802020.html