什么是此用法,应使用?它并没有解决哪些问题?使用 C + + 11 时是否更改用法?

虽然它已提到的许多地方,但我们没有任何单数"它是什么"的问题和答案,那么这就是那个。下面是在前面提到的位置的部分列表︰

2010-07-19 08:42:09
问题评论:

从等 Sutter gotw.ca/gotw/059.htm

问得好和极好回答,但可能应该是社区 wiki 呢?

@Philipp,随着更多的贡献将由用户编辑的它最终将成为社区 wiki。但之前--为什么不是我们让 GMan 赢得一些信誉为他工作呢?

@DumbCoder︰ 噢,我忘记了,谢谢。@Philipp: arguable。:)只是不要向上-投票问题,我想,但虽然我知道这一想法仍然只是一个问题和答案。它恰巧就是同一个人。(很显然如果我每天都很有问题。)

最好使全左上解释为此用法,就很少,每个人都应该知道关于它。

回答:

概述

我们为什么需要副本交换用法?

管理资源的任何类 (包装,像智能指针) 需要实现大三简单的目标和实现的复制构造函数和析构函数时,复制赋值运算符可以说是最能体现细微差别和困难。它如何应该?需要避免什么缺陷?

副本交换惯用语是解决方案,,像帮助赋值运算符实现两件事︰ 避免代码重复,并提供强大的异常保证.

它是如何工作的?

从概念上讲,工作原理是使用复制构造函数的功能与swap功能,换旧数据和新数据创建的数据,然后将复制的数据的本地副本。临时副本然后因,采取与之的旧数据。我们仍然面临的新的数据的副本。

为了使用副本交换方法,我们需要三样东西︰ 工作拷贝构造函数、 析构函数工作 (同时是任何包装的基础,因此应会完成吗) 和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]() : 0)
    {
    }

    // copy-constructor
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : 0),
    {
        // 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 = 0; // (2) *(see footnote for rationale)

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

    return *this;
}

然后我们说,我们已完成;此现在管理的数组,不泄漏。但是,它还受到了三个问题,在(n)作为代码中的顺序标记.

  1. 第一种是自我分配测试。这种检查有两个作用︰ 它是防止我们在自我赋值,运行不必要的代码的简单方法而且也保护我们从细微错误 (例如,删除仅要尝试,并将其复制的数组)。但在所有其他情况下则只是用来减慢程序,并作为噪声在代码中;自我分配很少发生,因此,大多数的这种检查是浪费时间。如果操作员无法正常工作而不,会更好。

  2. 第二是它仅提供了基本的异常保证。如果new int[mSize]失败, *this将被修改。(即,大小是错误和数据都丢失了 !)强的异常保证,它需要有 akin 到︰

    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]() : 0; // (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. 扩展代码了 !这会导致我们第三个问题︰ 重复的代码。我们赋值运算符有效地复制所有的代码已编写了在别处,就是一件可怕的事情。

在我们的例子中,它的核心是只有两个行 (分配和副本),但此代码的膨胀速度可以用更复杂的资源是相当多的麻烦。我们应努力不重复自己。

(一个可能想知道︰ 如果这么多的代码需要正确地管理一个资源,如果我类管理多个吗?虽然这看起来似乎是言之有理的并确实需要尽力try/catch子句,这是不成问题。这是因为类应管理只有一个资源!)

成功的解决方案

如上所述,副本交换方法可解决所有这些问题。但现在,我们有除一个以外的所有要求︰swap函数。虽然规则的三种成功必然存在的我们的复制构造函数、 析构函数和赋值运算符,它应真正调用"大三和一半": 类管理资源的任何时间也有意义提供了swap函数。

我们需要我们的类来添加交换功能,我们这样做是为 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 classes,
        // the two classes are effectively swapped
        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;
}

就是这样 !与一个 fell 内容,所有三个问题是雅观处理一次。

它为什么工作?

我们第一次发现一种重要的选择︰ 采取按值参数传递。而一个简单的可以做以下 (事实上,惯用语的许多天真实现执行)︰

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

    return *this;
}

我们失去了一个重要的优化机会不只,但此次的选择是关键在 C + + 11 中,稍后讨论。(常规说明一点,一个非常有用的原则是,如下所示︰ 如果您打算在函数中进行一份东西,让其在参数列表中执行编译器。 ‡)

无论如何,此种获取我们资源的方法是避免代码重复的关键︰ 我们可以使用复制构造函数中的代码来创建副本,并永远不需要重复它的任何位。现在,进行复制,我们就可供换用。

观察,进入已经分配所有的新数据,该函数复制,并已准备就绪。这是什么给了我们强大的异常保证免费︰ 如果构造的复制将失败,并且因此是不可能改变的状态,我们甚至根本不愿进入函数*this(什么我们做前手动强异常保证,现在; 如何,对我们做的编译器类型。)

此时,我们已开始释放,因为swap非引发。交换所复制的数据与当前数据安全地改变我们的状态,并将旧数据获取放入临时。当函数返回时,旧的数据然后被释放。(其中参数的作用域时结束,其析构函数被称为。

惯用语不重复任何代码,因为我们不能引入 bug 中的运算符。请注意,这意味着我们要将不需要进行自我赋值检查,允许单个统一实现的operator=(此外,我们不再拥有性能非自行分配。

而这是副本交换惯用语。

C + + 11 呢?

下一版本的 c + +、 C + + 11,使一个非常重要的更改到我们如何管理资源︰ 规则的三个是现在规则 4 (和半)。为什么?因为我们需要不仅能够复制构造我们的资源,我们需要它以及移动构造.

幸运的是对于我们而言,这很简单︰

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能够不要相同后换。

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

为什么工作?

就是我们需要对我们的类进行的唯一更改那么为什么它工作?请记住我们进行以一个值并不是引用参数的以往任何时候都重要决策︰

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

现在,如果other正在初始化 rvalue,它将是移动构造带。完美。在相同的方式 C + + 03 让我们重新使用我们的复制构造函数的功能通过获取由值参数,C + + 11 将自动选取移动构造函数在适当的时候也。(而且,当然,根据以前链接的文章中提到,复制/移动的值可能只是被省略了完全。

并因此得出结论的副本交换用法。


脚注

* 为什么我们设置为mArray因为如果任何进一步代码中运算符将引发,可以称为dumb_array的析构函数;并不将它设置为 null 的情况下发生这种情况,如果我们试图删除已删除的内存 !我们避免这种情况通过设置它为 null,因为删除空无操作。

†There 是另宣称我们应该专门为我们类型std::swap ,提供同类swap沿侧面释放函数swap等。但这是所有不必要︰swap任何正确使用将通过非限定的调用,并将通过ADL找到我们的函数。一个函数即可。

‡The 的原因很简单︰ 一旦对自己的资源,您可能会交换和/或将其移 (C + + 11) 任何地方需要。并通过在参数列表中进行复制,您最大限度地优化。

此外请注意,复制-和-交换还需要 dtor。(实际上,是显而易见的但由于你刚才提到的 cctor,可能也提到 dtor)。

@GMan︰ 我认为同时管理多个资源类是注定会失败 (异常安全性成为 nightmarish),我强烈建议,一种管理一个资源或有业务的功能和使用的管理人员。

@FrEEzE:"它的排列顺序列表中特定编译器进行处理"。不它不是。它是在类定义中出现的顺序进行处理。不接受std::copy这样的编译器被破坏,我不编码可用于断开编译器。和我不敢肯定我能理解您的最后一条批注。

未获得 swap 方法被声明为 friend 此处的原因吗?

@neuviemeporte︰ 与括号,数组元素的默认初始化。不,它们是未初始化。由于复制构造函数中我们将会覆盖值吗,我们可以跳过初始化。

分配,其实质上的是两个步骤︰对象的旧状态下撕裂和某些对象的状态生成一个副本作为其新的状态

从根本上说,这是析构函数复制构造函数的,因此,第一个想法就是委托给他们的工作。但是,由于销毁不失败,而将可能建设,我们实际上要做它周围的其他方式第一次执行建设性的一部分,如果已成功完成,则执行破坏性的一部分副本交换用法是一种方法做到这一点︰ 它首先调用类的复制构造函数来创建临时变量,然后调换其数据与临时数据,然后让临时的析构函数销毁原来的状态。
swap()应该永远不会失败,因为它可能会失败的唯一部分是复制构造。首先,执行,和没有任何目标对象中如果失败,将会更改。

以其精致的形式,由初始化 (非参考) 参数赋值运算符执行拷贝实现副本交换︰

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

我想一提 pimpl 是与涉及复制、 交换和销毁同样重要。换用不是神奇的异常安全。这是异常的因为换指针不会出现异常。您不具有使用 pimpl,但是如果不这样做则必须确保每个成员的交换是异常安全。当这些成员可以更改时它们 pimpl 背后隐藏的很重要,这可能是恶梦一场。然后,然后,然后是的 pimpl 成本。它引导我们的结论是,它往往异常安全性所携带的性能的成本。

...可以编写的类将保持 pimpl 的成本分摊的分配器。它会增加复杂性,纯香草副本交换方法的简洁性点击次数。它是一个选择。

std::swap(this_string, that)不提供无抛出保证。它提供了强大的异常安全,但不是无抛出保证。

@wilhelmtell︰ 在 C + + 03,并没有提及std::string::swap (这由std::swap) 可能引发的异常。在 C + + 0x, std::string::swapnoexcept ,且必须不引发异常。

@wilhelmtell︰ 我认为这是换点︰ 它永远不会引发,并始终是 o (1) (是的我知道, std::array...)

已存在一些很好的答案。我将重点主要是我认为它们缺乏的"缺点"与副本交换用法的解释...

什么是复制交换用法?

一种实现方面交换函数赋值运算符︰

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

基本思路是︰

  • 分配对象的最容易出错的部分确保新的状态需要的任何资源收购 (例如内存、 描述符)

  • 收购,可以尝试之前修改该对象的当前状态 (即*this) 如果新值的一个副本,则这是rhs的原因接受按值(即复制) 而不是通过引用

  • 换用的本地状态复制rhs*this通常相对较容易做到并且无潜在故障例外,给定的本地副本不需要进行任何特定状态以后 (运行,大部分与来自中移动对象的析构函数只需要适应状态 > = C + + 11)

应该何时使用?(哪些问题会解决[/ 创建]?)

  • 当您想分配到对象的赋值会引发异常,不会影响时,假定您有或只能写swap与强异常保证和理想的情况是一个不能进行故障 /throw.

  • 如果要干净整洁、 易于理解的、 可靠的方式来定义赋值运算符 (简单) 的复制构造函数、swap和析构函数方面。

    • 副本交换可以避免这个常被忽视的边缘情况下进行自我赋值。 ‡

  • 当任何性能降低或创建期间分配额外临时对象通过瞬间高资源使用率不重要为您的应用程序。

swap引发︰ 通常可以可靠地交换中通过指针,跟踪对象的数据成员,但没有引发自由交换的非指针数据成员,或者用于哪些交换必须作为实现X tmp = lhs; lhs = rhs; rhs = tmp;复制构造或工作分配可能引发,因此仍有可能对失败离开交换某些数据成员和其他人不和。这种可能性甚至应用到 C + + 03 std::string的 James 注释上另一个答案︰

@wilhelmtell︰ 在 C + + 03,并没有提及 std::string::swap (这由 std::swap) 可能引发的异常。在 C + + 0x,std::string::swap 是 noexcept,且必须不引发异常。--James McNellis 10 年 12 月 22日"在 15:24


轻松地自我赋值失败似乎清醒时分配一个不同的对象从 ‡ 赋值运算符实现。虽然它似乎是不可想像的客户端代码甚至会尝试自我赋值,也可能相对轻松地容器,算法操作过程与x = f(x); f的代码 (或许仅对某些#ifdef分支) 宏照#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_然后尝试其中读取uninitialised的数据 (未定义的行为),如果它并不会太奇怪,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 ,而副本交换方法将调用复制构造函数的很可能会被编写为打开不同的套接字连接,然后关闭原始。不仅可能意味着交互而不是简单的进程内变量副本的远程网络,它可以在套接字资源或连接上运行 afoul 的客户端或服务器的限制。(此类当然有相当是太可怕的接口,但这是另一回事;-P)。

这就是说,一个套接字连接时举的一个例子-相同的原则适用于任何可能很昂贵的初始化,如硬件探测/初始化/校准,生成池的线程或随机编号、 某些加密任务、 缓存、 文件系统扫描、 数据库连接等.

没有更多一个 (巨大) 的骗子。从技术上讲对象将从当前起规范没有移动赋值运算符 !如果以后用作类,新的类成员的不会自动生成移动构造函数 !来源︰ youtu.be/mYrbivnruYw?t=43m14s

Client复制赋值运算符的主要问题是工作分配未被禁止。

此答案很像加法和上面的回答需要稍做修改。

在某些版本的 Visual Studio (和可能的其它编译器) 没有一个 bug,是真正令人讨厌,毫无意义。因此,如果您声明/定义swap函数如下︰

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

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

}

...swap函数调用时,编译器将在您欢呼︰

enter image description here

这有一些与被调用的friend函数和this对象作为参数传递。


围绕这一种方法是使用friend关键字并重新定义swap函数︰

void swap(A& other) {

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

}

这一次,可以仅调用swap并传入other,从而使编译器快乐︰

enter image description here


毕竟,您不需要使用friend函数来换 2 的对象。其意义不仅仅是尽可能多地进行swap的成员函数中有一个other对象作为参数。

已访问this对象,以便将作为参数传递给它中是从技术上讲多余。

您可以共享您的示例用于重现该错误?

@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp dropbox.com/s/jrjrn5dh1zez5vy/Untitled.jpg这是一个简化的版本。一个错误似乎出现每次的friend函数调用与*this参数

@GManNickG 它不太适合所有图像和代码示例的说明。确定,如果人 downvote,我确信没有人那里获得的相同的错误;这篇文章中的信息可能只是他们需要的东西。

请注意,这仅突出显示 (IntelliSense) IDE 代码中的错误...将编译而不警告/错误很正常。

请将报告与此处错误如果您有没试过 (和尚未解决) connect.microsoft.com/VisualStudio

我想添加警告,在处理与 C + + 11 式分配器的容器。交换和分配具有稍有不同的语义。

对于 concreteness,让我们考虑容器std::vector<T, A>,其中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<A>.

  • 如果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 项重要操作一旦容器开始支持有状态的分配器。这是一个有点"高级的使用案例",但它不是完全不可能的因为移动优化通常只会变得有趣一旦您类管理资源,并且内存将最受欢迎的资源之一。

内容来源于Stack Overflow What is the copy-and-swap idiom?
请输入您的翻译

What is the copy-and-swap idiom?

确认取消