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

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

问题描述

这个成语是什么,什么时候用?它解决了哪些问题?使用 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?

copy-and-swap idiom 是解决方案,它优雅地协助赋值运算符实现两件事:避免 代码重复,并提供强大的异常保证一>.

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.

概念上,它通过使用复制构造函数的功能是创建数据的本地副本,然后使用 swap 函数获取复制的数据,将旧数据与新数据交换.然后临时副本销毁,同时带走旧数据.我们留下了一份新数据的副本.

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.

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

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!

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

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

让我们考虑一个具体的案例.我们想在一个其他无用的类中管理一个动态数组.我们从一个有效的构造函数、复制构造函数和析构函数开始:

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

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

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

以下是简单实现的样子:

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

然后我们说我们完成了;这现在管理一个数组,没有泄漏.然而,它存在三个问题,在代码中依次标记为(n).

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. 首先是自我分配测试.
    这个检查有两个目的:它是一种防止我们在自赋值时运行不必要代码的简单方法,它保护我们免受细微的错误(例如删除数组只是为了尝试和复制它).但在所有其他情况下,它只会减慢程序的速度,并在代码中充当噪音;自赋值很少发生,所以大部分时间这个检查都是浪费.
    如果操作员没有它也能正常工作就更好了.

  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.

第二个是它只提供基本的异常保证.如果 new int[mSize] 失败,*this 将被修改.(即大小不对,数据没了!)
对于强大的异常保证,它需要类似于:

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

  • 代码已展开!这就引出了第三个问题:代码重复.

  • 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.

    (有人可能会问:如果正确管理一个资源需要这么多代码,如果我的类管理多个怎么办?
    虽然这似乎是一个有效的问题,而且确实需要非平凡的 try/catch 子句,但这不是问题.
    这是因为一个类应该管理仅一个资源!)

    (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!)

    如前所述,复制和交换习语将解决所有这些问题.但是现在,除了一个swap 函数之外,我们有所有的要求.虽然三法则成功地包含了我们的复制构造函数、赋值运算符和析构函数,但它确实应该被称为三巨头":任何时候你的类管理一个资源,提供一个swap 函数.

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

    (这里解释了为什么公开好友交换.) 现在我们不仅可以交换 dumb_array 的,而且交换通常可以更有效;它只是交换指针和大小,而不是分配和复制整个数组.除了功能和效率方面的这一优势外,我们现在准备实施复制和交换习语.

    (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.

    我们首先注意到一个重要的选择:参数参数是按值.虽然人们可以很容易地做到以下几点(事实上,这个习语的许多幼稚的实现都是这样做的):

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

    我们失去了一个 重要的优化机会.不仅如此,这个选择在 C++11 中也很关键,后面会讨论.(一般来说,一个非常有用的准则如下:如果您要在函数中复制某些内容,请让编译器在参数列表中进行复制.‡)

    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.

    在进入函数时观察到,所有新数据都已分配、复制并准备好使用.这就是免费为我们提供强大异常保证的原因:如果复制的构造失败,我们甚至不会进入该函数,因此不可能更改 *this 的状态.(我们之前手动为强异常保证所做的,现在编译器正在为我们做;怎么样.)

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

    此时我们无家可归,因为 swap 是非抛出的.我们用复制的数据交换当前数据,安全地改变我们的状态,旧数据被放入临时数据.然后在函数返回时释放旧数据.(在参数的作用域结束并调用其析构函数的地方.)

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

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

    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.

    C++ 的下一个版本 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.

    幸运的是,这很容易:

    class dumb_array
    {
    public:
        // ...
    
        // move constructor
        dumb_array(dumb_array&& other) noexcept ††
            : 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.

    所以我们所做的很简单:通过默认构造函数(C++11 特性)初始化,然后与other 交换;我们知道我们类的默认构造实例可以安全地分配和销毁,因此我们知道 other 将能够在交换后执行相同的操作.

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

    这是我们需要对类进行的唯一更改,那么它为什么会起作用呢?记住我们做出的让参数成为值而不是引用的重要决定:

    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)
    

    现在,如果 other 用右值初始化,它将被移动构造.完美的.与 C++03 让我们通过按值获取参数来重用我们的复制构造函数功能一样,C++11 也会在适当的时候自动选择移动构造函数.(当然,正如之前链接的文章中提到的,值的复制/移动可能会被完全省略.)

    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.

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

    *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.

    †还有其他说法认为我们应该为我们的类型专门化 std::swap,提供类内 swap 和自由函数 swap 等.但这都是不必要的:任何对 swap 的正确使用都将通过不合格的调用,而我们的函数将通过 ADL.一个功能就行.

    †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.

    ‡原因很简单:一旦您拥有自己的资源,您就可以将其交换和/或移动 (C++11) 到任何需要的地方.通过在参数列表中进行复制,您可以最大限度地优化.

    ‡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.

    ††移动构造函数通常应该是 noexcept,否则一些代码(例如 std::vector 调整大小逻辑)将使用复制构造函数,即使移动会使感觉.当然,只有在里面的代码不抛出异常的情况下才标记为noexcept.

    ††The move constructor should generally be noexcept, otherwise some code (e.g. std::vector resizing logic) will use the copy constructor even when a move would make sense. Of course, only mark it noexcept if the code inside doesn't throw exceptions.

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

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