什么是复制和交换习语?

发布于 2024-09-10 10:02:31 字数 676 浏览 4 评论 0 原文

什么是复制和交换习惯用法以及何时应该使用它?它解决什么问题? C++11 会改变吗?

相关:

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

冷心人i 2024-09-17 10:02:31

概述

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

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

复制和交换惯用语是解决方案,它优雅地帮助赋值运算符实现两件事:避免代码重复,并提供强大的异常保证

它是如何运作的?

从概念上讲,它通过使用复制构造函数来工作创建数据的本地副本的功能,然后使用 swap 函数获取复制的数据,将旧数据与新数据交换。然后,临时副本将被破坏,并带走旧数据。我们留下了新数据的副本。

为了使用复制和交换习惯用法,我们需要三样东西:一个工作复制构造函数、一个工作析构函数(两者都是任何包装器的基础,因此无论如何都应该是完整的)和一个交换代码>函数。

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

(不仅如此,对 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)

  1. 第一个是自我分配测试。
    此检查有两个目的:它是防止我们在自分配时运行不必要的代码的简单方法,并且它可以保护我们免受细微错误的影响(例如仅删除数组以尝试复制它)。但在所有其他情况下,它只会减慢程序速度,并充当代码中的噪音;自分配很少发生,所以大多数时候这种检查是浪费。
    如果没有它操作员也能正常工作那就更好了。

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

    dumb_array&运算符=(常量dumb_array&amp;其他)
     {
         if (这个!= &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)
    
             // 替换旧数据(都是不抛的)
             删除[] mArray;
             mSize = 新大小;
             mArray = newArray;
         }
    
         返回*这个;
     }
    
  3. 代码已扩展!这就引出了第三个问题:代码重复。

我们的赋值运算符实际上复制了我们已经在其他地方编写的所有代码,这是一件可怕的事情。

在我们的例子中,它的核心只有两行(分配和复制),但是对于更复杂的资源,这种代码膨胀可能会很麻烦。我们应该努力不重复自己。

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

一个成功的解决方案

如前所述,复制和交换习惯将解决所有这些问题。但现在,我们已经满足了所有要求,除了一个:交换函数。虽然“三法则”成功地保证了复制构造函数、赋值运算符和析构函数的存在,但它实际上应该被称为“三大半”:任何时候您的类管理资源时,提供 <代码>交换功能。

我们需要向我们的类添加交换功能,具体操作如下†:(

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,而且交换通常会更高效;它只是交换指针和大小,而不是分配和复制整个数组。除了功能和效率方面的优势之外,我们现在已经准备好实现复制和交换习惯。

言归正传,我们的赋值运算符是:

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

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

无论哪种方式,这种获取资源的方法是消除代码重复的关键:我们可以使用复制构造函数中的代码来进行复制,并且永远不需要重复其中的任何部分。现在副本已制作完毕,我们准备好进行交换了。

请注意,进入该函数后,所有新数据都已分配、复制并准备好使用。这为我们提供了强大的免费异常保证:如果副本构造失败,我们甚至不会进入该函数,因此无法更改 *this 的状态。 (我们之前手动为强大的异常保证所做的事情,现在编译器正在为我们做;多好啊。)

在这一点上,我们是空闲的,因为 swap 是非抛出的。我们将当前数据与复制的数据交换,安全地改变我们的状态,并将旧数据放入临时数据中。当函数返回时,旧数据被释放。 (参数的作用域结束并调用其析构函数。)

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

这就是复制和交换的习惯用法。

C++11 怎么样?

C++ 的下一个版本,C++11,对我们管理资源的方式做出了一个非常重要的改变:三规则现在是四规则(半)。为什么?因为我们不仅需要能够复制构造我们的资源,我们还需要移动构造它

幸运的是,这对我们来说很简单:

class dumb_array
{
public:
    // ...

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

    // ...
};

这里发生了什么?回想一下移动构造的目标:从类的另一个实例获取资源,使其处于保证可分配和可破坏的状态。

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

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

为什么会这样?

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

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

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

复制和交换习惯用法就这样结束了。


脚注

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

†还有其他主张,我们应该为我们的类型专门化 std::swap,提供类内 swap 以及自由函数 swap但这都是不必要的:任何对 swap 的正确使用都将通过非限定调用进行,并且我们的函数将通过 ADL。一个函数就可以了。

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

††移动构造函数通常应为 noexcept,否则某些代码(例如 std::vector 调整大小逻辑)将使用复制构造函数,即使移动有意义。当然,只有当里面的代码不抛出异常时才将其标记为no except。

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

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.

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

吃不饱 2024-09-17 10:02:31

赋值的核心是两个步骤:拆除对象的旧状态将其新状态构建为副本

基本上,这就是析构函数复制构造函数所做的,所以第一个想法是委托工作给他们。然而,由于破坏不能失败,而构造可能会失败,我们实际上想以相反的方式进行首先执行构造部分如果成功,则执行破坏性部分。复制和交换习惯用法就是这样做的一种方法:它首先调用类的复制构造函数来创建临时对象,然后将其数据与临时对象交换,然后让临时对象的析构函数销毁旧状态。
由于 swap() 应该永远不会失败,因此唯一可能失败的部分是复制构造。首先执行此操作,如果失败,则目标对象中不会发生任何更改。

在其精炼形式中,复制和交换是通过初始化赋值运算符的(非引用)参数来执行复制来实现的:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}

Assignment, at its heart, is two steps: tearing down the object's old state and building its new state as a copy of some other object's state.

Basically, that's what the destructor and the copy constructor do, so the first idea would be to delegate the work to them. However, since destruction mustn't fail, while construction might, we actually want to do it the other way around: first perform the constructive part and, if that succeeded, then do the destructive part. The copy-and-swap idiom is a way to do just that: It first calls a class' copy constructor to create a temporary object, then swaps its data with the temporary's, and then lets the temporary's destructor destroy the old state.
Since swap() is supposed to never fail, the only part which might fail is the copy-construction. That is performed first, and if it fails, nothing will be changed in the targeted object.

In its refined form, copy-and-swap is implemented by having the copy performed by initializing the (non-reference) parameter of the assignment operator:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}
救星 2024-09-17 10:02:31

已经有一些很好的答案了。我将主要关注我认为他们缺少的东西 - 用复制和交换惯用法对“缺点”的解释......

什么是复制和交换习惯用法?

根据交换函数实现赋值运算符的一种方法:

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}

基本思想是:

  • 分配给对象时最容易出错的部分是确保获取新状态所需的任何资源(例如内存、描述符)

  • 在修改当前状态之前可以尝试获取该资源如果创建了新值的副本,则对象的状态(即 *this),这就是为什么 rhs按值接受(即复制)而不是通过引用

  • 交换本地副本 rhs*this 的状态通常相对容易实现,没有潜在的失败/异常,因为本地副本不存在之后需要任何特定状态(只需要适合析构函数运行的状态,就像从 >= C++11 中移动的对象一样)

什么时候应该使用它? (它解决了哪些问题[/create]?)

  • 当您希望被分配的对象不受引发异常的分配的影响时,假设您拥有或可以编写交换 具有强大的异常保证,并且理想情况下不会失败/抛出..†

  • 当您想要一个干净、简单的理解根据(更简单的)复制构造函数、交换和析构函数定义赋值运算符的可靠方法。

    • 以复制和交换方式完成的自分配避免了经常被忽视的边缘情况。‡

  • 当在分配期间使用额外的临时对象而造成的任何性能损失或暂时较高的资源使用率对您的应用程序并不重要时。 ⁂

swap 抛出:通常可以可靠地交换对象通过指针跟踪的数据成员,但对于没有无抛出交换的非指针数据成员,或者必须进行交换的非指针数据成员实现为 X tmp = lhs;左轴 = 右轴; rhs = tmp; 并且复制构造或赋值可能会抛出异常,仍然有可能失败,导​​致某些数据成员被交换而另一些则没有。这种潜力甚至适用于 C++03 std::string ,正如 James 对另一个答案的评论:

@wilhelmtell:在 C++03 中,没有提及 std::string::swap (由 std::swap 调用)可能引发的异常。在 C++0x 中,std::string::swap 是 noexcept 并且不能抛出异常。 – 詹姆斯·麦克内利斯,2010 年 12 月 22 日 15:24


‡ 当从不同对象进行分配时,看似正常的赋值运算符实现很容易因自分配而失败。虽然客户端代码甚至尝试自分配似乎难以想象,但在容器上的算法操作期间,这种情况相对容易发生,使用 x = f(x); 代码,其中 f 是(可能仅适用于某些 #ifdef 分支)宏 ala #define f(x) x 或返回对 x 引用的函数,或者甚至是像 x = c1 这样的代码(可能效率低下但简洁)? x * 2 : c2 ? x / 2 : x;)。例如:

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

在自分配时,上面的代码删除了 x.p_;,将 p_ 指向新分配的堆区域,然后尝试读取未初始化的< /em> 其中的数据(未定义的行为),如果这没有做任何太奇怪的事情,copy 会尝试对每个刚刚破坏的“T”进行自分配!


⁂ 由于使用额外的临时变量(当操作员的参数是复制构造的),复制和交换习惯用法可能会导致效率低下或受到限制:

struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

这里是一个手写的 Client::operator=可能会检查 *this 是否已连接到与 rhs 相同的服务器(如果有用,可能会发送“重置”代码),而复制和交换方法将调用复制构造函数可能会被编写为打开一个不同的套接字连接,然后关闭原始连接。这不仅意味着远程网络交互而不是简单的进程内变量复制,而且可能会违反客户端或服务器对套接字资源或连接的限制。 (当然,这个类有一个非常可怕的接口,但那是另一回事;-P)。

There are some good answers already. I'll focus mainly on what I think they lack - an explanation of the "cons" with the copy-and-swap idiom....

What is the copy-and-swap idiom?

A way of implementing the assignment operator in terms of a swap function:

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}

The fundamental idea is that:

  • the most error-prone part of assigning to an object is ensuring any resources the new state needs are acquired (e.g. memory, descriptors)

  • that acquisition can be attempted before modifying the current state of the object (i.e. *this) if a copy of the new value is made, which is why rhs is accepted by value (i.e. copied) rather than by reference

  • swapping the state of the local copy rhs and *this is usually relatively easy to do without potential failure/exceptions, given the local copy doesn't need any particular state afterwards (just needs state fit for the destructor to run, much as for an object being moved from in >= C++11)

When should it be used? (Which problems does it solve [/create]?)

  • When you want the assigned-to objected unaffected by an assignment that throws an exception, assuming you have or can write a swap with strong exception guarantee, and ideally one that can't fail/throw..†

  • When you want a clean, easy to understand, robust way to define the assignment operator in terms of (simpler) copy constructor, swap and destructor functions.

    • Self-assignment done as a copy-and-swap avoids oft-overlooked edge cases.‡
  • When any performance penalty or momentarily higher resource usage created by having an extra temporary object during the assignment is not important to your application. ⁂

swap throwing: it's generally possible to reliably swap data members that the objects track by pointer, but non-pointer data members that don't have a throw-free swap, or for which swapping has to be implemented as X tmp = lhs; lhs = rhs; rhs = tmp; and copy-construction or assignment may throw, still have the potential to fail leaving some data members swapped and others not. This potential applies even to C++03 std::string's as James comments on another answer:

@wilhelmtell: In C++03, there is no mention of exceptions potentially thrown by std::string::swap (which is called by std::swap). In C++0x, std::string::swap is noexcept and must not throw exceptions. – James McNellis Dec 22 '10 at 15:24


‡ assignment operator implementation that seems sane when assigning from a distinct object can easily fail for self-assignment. While it might seem unimaginable that client code would even attempt self-assignment, it can happen relatively easily during algo operations on containers, with x = f(x); code where f is (perhaps only for some #ifdef branches) a macro ala #define f(x) x or a function returning a reference to x, or even (likely inefficient but concise) code like x = c1 ? x * 2 : c2 ? x / 2 : x;). For example:

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

On self-assignment, the above code delete's x.p_;, points p_ at a newly allocated heap region, then attempts to read the uninitialised data therein (Undefined Behaviour), if that doesn't do anything too weird, copy attempts a self-assignment to every just-destructed 'T'!


⁂ The copy-and-swap idiom can introduce inefficiencies or limitations due to the use of an extra temporary (when the operator's parameter is copy-constructed):

struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

Here, a hand-written Client::operator= might check if *this is already connected to the same server as rhs (perhaps sending a "reset" code if useful), whereas the copy-and-swap approach would invoke the copy-constructor which would likely be written to open a distinct socket connection then close the original one. Not only could that mean a remote network interaction instead of a simple in-process variable copy, it could run afoul of client or server limits on socket resources or connections. (Of course this class has a pretty horrid interface, but that's another matter ;-P).

以歌曲疗慰 2024-09-17 10:02:31

这个答案更像是对上面答案的补充和轻微修改。

在某些版本的 Visual Studio(可能还有其他编译器)中,存在一个非常烦人且没有意义的错误。因此,如果您像这样声明/定义 swap 函数:

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

...当您调用 swap 函数时,编译器会对您大喊大叫:

在此处输入图像描述

这与调用 friend 函数和传递 this 对象有关作为参数。


解决这个问题的方法是不使用 friend 关键字并重新定义 swap 函数:

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

这一次,您只需调用 swap 并传入 >other,从而使编译器满意:

在此处输入图像描述


毕竟,您不需要使用friend函数来交换2个对象。将 swap 设为一个以一个 other 对象作为参数的成员函数同样有意义。

您已经可以访问 this 对象,因此将其作为参数传递在技术上是多余的。

This answer is more like an addition and a slight modification to the answers above.

In some versions of Visual Studio (and possibly other compilers) there is a bug that is really annoying and doesn't make sense. So if you declare/define your swap function like this:

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

... the compiler will yell at you when you call the swap function:

enter image description here

This has something to do with a friend function being called and this object being passed as a parameter.


A way around this is to not use friend keyword and redefine the swap function:

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

This time, you can just call swap and pass in other, thus making the compiler happy:

enter image description here


After all, you don't need to use a friend function to swap 2 objects. It makes just as much sense to make swap a member function that has one other object as a parameter.

You already have access to this object, so passing it in as a parameter is technically redundant.

緦唸λ蓇 2024-09-17 10:02:31

当您处理 C++11 样式的分配器感知容器时,我想添加一句警告。交换和赋值的语义略有不同。

为了具体起见,让我们考虑一个容器 std::vector,其中 A 是某种有状态分配器类型,我们将比较以下函数

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

:函数 fsfm 的目的都是为 a 提供 b 最初的状态。然而,有一个隐藏的问题:如果a.get_allocator() != b.get_allocator()会发生什么?答案是:视情况而定。让我们编写AT = std::allocator_traits

  • 如果 AT::propagate_on_container_move_assignmentstd::true_type,则 fm 重新分配 a 的分配器b.get_allocator() 的值,否则不会,并且 a 继续使用其原始分配器。在这种情况下,需要单独交换数据元素,因为 ab 的存储不兼容。

  • 如果 AT::propagate_on_container_swapstd::true_type,则 fs 以预期方式交换数据和分配器。

  • 如果AT::propagate_on_container_swapstd::false_type,那么我们需要动态检查。

    • 如果a.get_allocator() == b.get_allocator(),则两个容器使用兼容的存储,并以通常的方式进行交换。
    • 但是,如果a.get_allocator() != b.get_allocator(),则程序具有未定义的行为(参见[container.requirements.general/8] .

结果是,一旦您的容器开始支持有状态分配器,交换就成为 C++11 中的一项重要操作,这有点“高级用例”,但这并非完全不可能,因为移动优化通常只有在您的类时才变得有趣。管理资源,而内存是最流行的资源之一。

I would like to add a word of warning when you are dealing with C++11-style allocator-aware containers. Swapping and assignment have subtly different semantics.

For concreteness, let us consider a container std::vector<T, A>, where A is some stateful allocator type, and we'll compare the following functions:

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

The purpose of both functions fs and fm is to give a the state that b had initially. However, there is a hidden question: What happens if a.get_allocator() != b.get_allocator()? The answer is: It depends. Let's write AT = std::allocator_traits<A>.

  • If AT::propagate_on_container_move_assignment is std::true_type, then fm reassigns the allocator of a with the value of b.get_allocator(), otherwise it does not, and a continues to use its original allocator. In that case, the data elements need to be swapped individually, since the storage of a and b is not compatible.

  • If AT::propagate_on_container_swap is std::true_type, then fs swaps both data and allocators in the expected fashion.

  • If AT::propagate_on_container_swap is std::false_type, then we need a dynamic check.

    • If a.get_allocator() == b.get_allocator(), then the two containers use compatible storage, and swapping proceeds in the usual fashion.
    • However, if a.get_allocator() != b.get_allocator(), the program has undefined behaviour (cf. [container.requirements.general/8].

The upshot is that swapping has become a non-trivial operation in C++11 as soon as your container starts supporting stateful allocators. That's a somewhat "advanced use case", but it's not entirely unlikely, since move optimizations usually only become interesting once your class manages a resource, and memory is one of the most popular resources.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文