什么是复制和交换惯用语? [英] What is the copy-and-swap idiom?

查看:163
本文介绍了什么是复制和交换惯用语?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



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




解决方案

概述



复制和交换成语?



管理资源的任何类(一个 wrapper ,就像一个智能指针)需要实现大三。虽然复制构造函数和析构函数的目标和实现是直接的,但是复制赋值运算符可以说是最微妙和困难的。应该如何做呢?



是复制和交换惯用语的解决方案,可以帮助赋值运算符实现两个目的:避免代码重复,并提供强异常保证



如何运作?



在概念上,它通过使用复制构造函数的功能来创建数据的本地副本,然后使用 swap 函数获取复制的数据,使用新数据交换旧数据。然后临时副本会破坏,用旧的数据。我们留下了一个新数据的副本。



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



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

(不仅如此,对 swap 的非限定调用将使用我们的自定义交换操作符,跳过不必要的构造和销毁 std :: swap 会包含。)






深入解释



目标



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

  #include< algorithm& // std :: copy 
#include< cstddef> // std :: size_t

class dumb_array
{
public:
//(默认)构造函数
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);
}

//析构函数
〜dumb_array()
{
delete [] mArray;
}

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

这个类几乎成功地管理数组,但需要 operator = 可以正常工作。



一个失败的解决方案



  //硬的部分
dumb_array& operator =(const dumb_array& other)
{
if(this!=& other)//(1)
{
//删除旧数据。
delete [] mArray; //(2)
mArray = nullptr; //(2)*(见脚注的理由)

// ...并放入新的
mSize = other.mSize; //(3)
mArray = mSize? new int [mSize]:nullptr; //(3)
std :: copy(other.mArray,other.mArray + mSize,mArray); //(3)
}

return * this;
}

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


  1. 第一个是自我测试。这种检查有两个目的:这是一个简单的方法,以防止我们在自我分配上运行不必要的代码,它保护我们免受微妙的错误(例如删除数组只是尝试和复制它)。但在所有其他情况下,它只是用来减慢程序,并在代码中作为噪声;自我分配很少发生,所以大部分时间这种检查是浪费。


  2. 第二个是它只提供一个基本的异常保证。如果 new int [mSize] 失败, * this 将被修改。对于强大的异常保证,它需要类似于:

      dumb_array& operator =(const dumb_array& other)
    {
    if(this!=& other)//(1)
    {
    //旧的
    std :: size_t newSize = other.mSize;
    int * newArray = newSize? new int [newSize]():nullptr; //(3)
    std :: copy(other.mArray,other.mArray + newSize,newArray); //(3)

    //替换旧数据(都是非抛出的)
    delete [] mArray;
    mSize = newSize;
    mArray = newArray;
    }

    return * this;
    }


  3. 代码已扩展!这导致我们的第三个问题:代码重复。


在我们的代码中,我们的代码是一个非常复杂的代码。 case,其核心是只有两行(分配和副本),但是用更复杂的资源这个代码膨胀可以是相当麻烦。我们应该努力不再重复自己。



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



一个成功的解决方案



如上所述,复制和交换习语将解决所有这些问题。但现在,我们有所有的要求,除了一个: swap 函数。虽然三项规则成功地要求我们的复制构造函数,赋值运算符和析构函数的存在,它应该真正被称为大三和半:任何时候你的类管理一个资源,它也是有意义的提供一个 swap 函数。



我们需要向我们的类添加交换功能,我们这样做: p>

  class dumb_array 
{
public:
// ...

friend void swap(dumb_array& first,dumb_array& second)// nothrow
{
//启用ADL(在我们的例子中不是必需的,但是最好的做法)
使用std :: swap ;

//通过交换两个对象的成员
//两个对象被有效交换
swap(first.mSize,second.mSize);
swap(first.mArray,second.mArray);
}

// ...
};

这里是解释为什么 public friend swap 。)现在不仅可以交换我们的 dumb_array ,但是一般的交换可以更有效;它只是交换指针和大小,而不是分配和复制整个数组。除了这个功能和效率的奖励,我们现在已经准备好实现复制和交换惯例。



我们的赋值运算符是:

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

return * this;
}

就是这样!



为什么工作?



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

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

return * this;
}

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



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



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



现在我们是免费的, code> swap 是不抛出。我们使用复制的数据交换我们当前的数据,安全地更改我们的状态,并将旧数据放入临时数据中。然后当函数返回时释放旧数据。 (其中参数的作用域结束,它的析构函数被调用。)



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



这是复制和交换惯用法。



C ++ 11是什么?



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



幸运的是,这很容易:

  class dumb_array 
{
public:
// ...

//移动构造函数
dumb_array (dumb_array&&其他)
:dumb_array()//通过默认构造函数初始化,只有C ++ 11
{
swap(* this,other)
}

// ...
};

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



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



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



为什么这样工作?



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

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

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






$ b mArray 为null?因为如果操作符中有任何其他代码引发,可能会调用 dumb_array 的析构函数;如果这种情况没有设置为null,我们试图删除已经删除的内存!我们通过将其设置为null来避免这种情况,因为删除null是一个无操作。



†还有其他声明我们应该专门化 std: :swap 为我们的类型,提供一个类中 swap 旁边的一个自由函数 swap 等,但是这是没有必要的: swap 的任何正确使用将通过非限定调用,我们的函数将通过 ADL 。一个函数会做。



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


What is this idiom and when should it be used? Which problems does it solve? Does the idiom change when C++11 is used?

Although it's been mentioned in many places, we didn't have any singular "what is it" question and answer, so here it is. Here is a partial list of places where it was previously mentioned:

解决方案

Overview

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

Any class that manages a resource (a wrapper, like a smart pointer) needs to implement The Big Three. While the goals and implementation of the copy-constructor and destructor are straightforward, the copy-assignment operator is arguably the most nuanced and difficult. How should it be done? What pitfalls need to be avoided?

The copy-and-swap idiom is the solution, and elegantly assists the assignment operator in achieving two things: avoiding code duplication, and providing a strong exception guarantee.

How does it work?

Conceptually, it works by using the copy-constructor's functionality to create a local copy of the data, then takes the copied data with a swap function, swapping the old data with the new data. The temporary copy then destructs, taking the old data with it. We are left with a copy of the new data.

In order to use the copy-and-swap idiom, we need three things: a working copy-constructor, a working destructor (both are the basis of any wrapper, so should be complete anyway), and a swap function.

A swap function is a non-throwing function that swaps two objects of a class, member for member. We might be tempted to use std::swap instead of providing our own, but this would be impossible; std::swap uses the copy-constructor and copy-assignment operator within its implementation, and we'd ultimately be trying to define the assignment operator in terms of itself!

(Not only that, but unqualified calls to swap will use our custom swap operator, skipping over the unnecessary construction and destruction of our class that std::swap would entail.)


An in-depth explanation

The goal

Let's consider a concrete case. We want to manage, in an otherwise useless class, a dynamic array. We start with a working constructor, copy-constructor, and destructor:

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

This class almost manages the array successfully, but it needs operator= to work correctly.

A failed solution

Here's how a naive implementation might look:

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

And we say we're finished; this now manages an array, without leaks. However, it suffers from three problems, marked sequentially in the code as (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.

In our case, the core of it is only two lines (the allocation and the copy), but with more complex resources this code bloat can be quite a hassle. We should strive to never repeat ourselves.

(One might wonder: if this much code is needed to manage one resource correctly, what if my class manages more than one? While this may seem to be a valid concern, and indeed it requires non-trivial try/catch clauses, this is a non-issue. That's because a class should manage one resource only!)

A successful solution

As mentioned, the copy-and-swap idiom will fix all these issues. But right now, we have all the requirements except one: a swap function. While The Rule of Three successfully entails the existence of our copy-constructor, assignment operator, and destructor, it should really be called "The Big Three and A Half": any time your class manages a resource it also makes sense to provide a swap function.

We need to add swap functionality to our class, and we do that as follows†:

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 is the explanation why public friend swap.) Now not only can we swap our dumb_array's, but swaps in general can be more efficient; it merely swaps pointers and sizes, rather than allocating and copying entire arrays. Aside from this bonus in functionality and efficiency, we are now ready to implement the copy-and-swap idiom.

Without further ado, our assignment operator is:

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

    return *this;
}

And that's it! With one fell swoop, all three problems are elegantly tackled at once.

Why does it work?

We first notice an important choice: the parameter argument is taken by-value. While one could just as easily do the following (and indeed, many naive implementations of the idiom do):

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

    return *this;
}

We lose an important optimization opportunity. Not only that, but this choice is critical in C++11, which is discussed later. (On a general note, a remarkably useful guideline is as follows: if you're going to make a copy of something in a function, let the compiler do it in the parameter list.‡)

Either way, this method of obtaining our resource is the key to eliminating code duplication: we get to use the code from the copy-constructor to make the copy, and never need to repeat any bit of it. Now that the copy is made, we are ready to swap.

Observe that upon entering the function that all the new data is already allocated, copied, and ready to be used. This is what gives us a strong exception guarantee for free: we won't even enter the function if construction of the copy fails, and it's therefore not possible to alter the state of *this. (What we did manually before for a strong exception guarantee, the compiler is doing for us now; how kind.)

At this point we are home-free, because swap is non-throwing. We swap our current data with the copied data, safely altering our state, and the old data gets put into the temporary. The old data is then released when the function returns. (Where upon the parameter's scope ends and its destructor is called.)

Because the idiom repeats no code, we cannot introduce bugs within the operator. Note that this means we are rid of the need for a self-assignment check, allowing a single uniform implementation of operator=. (Additionally, we no longer have a performance penalty on non-self-assignments.)

And that is the copy-and-swap idiom.

What about C++11?

The next version of C++, C++11, makes one very important change to how we manage resources: the Rule of Three is now The Rule of Four (and a half). Why? Because not only do we need to be able to copy-construct our resource, we need to move-construct it as well.

Luckily for us, this is easy:

class dumb_array
{
public:
    // ...

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

    // ...
};

What's going on here? Recall the goal of move-construction: to take the resources from another instance of the class, leaving it in a state guaranteed to be assignable and destructible.

So what we've done is simple: initialize via the default constructor (a C++11 feature), then swap with other; we know a default constructed instance of our class can safely be assigned and destructed, so we know other will be able to do the same, after swapping.

(Note that some compilers do not support constructor delegation; in this case, we have to manually default construct the class. This is an unfortunate but luckily trivial task.)

Why does that work?

That is the only change we need to make to our class, so why does it work? Remember the ever-important decision we made to make the parameter a value and not a reference:

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

Now, if other is being initialized with an rvalue, it will be move-constructed. Perfect. In the same way C++03 let us re-use our copy-constructor functionality by taking the argument by-value, C++11 will automatically pick the move-constructor when appropriate as well. (And, of course, as mentioned in previously linked article, the copying/moving of the value may simply be elided altogether.)

And so concludes the copy-and-swap idiom.


Footnotes

*Why do we set mArray to null? Because if any further code in the operator throws, the destructor of dumb_array might be called; and if that happens without setting it to null, we attempt to delete memory that's already been deleted! We avoid this by setting it to null, as deleting null is a no-operation.

†There are other claims that we should specialize std::swap for our type, provide an in-class swap along-side a free-function swap, etc. But this is all unnecessary: any proper use of swap will be through an unqualified call, and our function will be found through ADL. One function will do.

‡The reason is simple: once you have the resource to yourself, you may swap and/or move it (C++11) anywhere it needs to be. And by making the copy in the parameter list, you maximize optimization.

这篇关于什么是复制和交换惯用语?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆