c - copy-and-swap用法是什么?

  显示原文与译文双语对照的内容

这个习惯用法是什么,什么时候应该使用它? 它解决了哪些问题? 使用C++11时,该习惯用法是否改变?

虽然在很多地方都提到过,但我们没有任何奇异的"它是什么"问题和答案,所以。 下面是前面提到的一些地方的部分列表:

时间:

概述

为什么我们需要copy-and-swap的习惯用法?

管理资源( ,如智能指针)的任何类都需要实现的三大 。 尽管copy-constructor和析构函数的目标和实现很简单,但是copy-assignment操作符是最微妙和最困难的。 应该如何完成? 需要避免的陷阱?

copy-and-swap的习惯用法是解决方案,它优雅地帮助赋值操作符实现了两个方面: 避免代码重复,并提供强异常保证。

它是如何工作的?

在概念上是,它通过使用copy-constructor的功能来创建一个本地副本,然后使用 swap 函数来复制数据,并用新数据交换旧数据。 临时副本,然后 destructs,取出旧数据。 我们留下了新数据的副本。

为了使用copy-and-swap习惯用法,我们需要三个东西: 一个工作的copy-constructor,一个工作的析构函数( 两者都是任何包装的基础,所以应该是完整的) 和一个 swap 函数。

于member,相关交换空间的功能是一个 non-throwing 函数并交换类的两个对象,member. 我们可能会尝试使用 std::swap 而不是提供自己的代码,但这可能是不可能的;std::swap 在它的实现中使用copy-constructor和copy-assignment操作符,我们最终会尝试在它本身中定义赋值运算符 !

( 不仅如此,对 swap的未限定调用将使用我们自定义的交换运算符,跳过不必要的构造和 std::swap 将需要的类的破坏。)


深入的解释

目标

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


#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) 顺序标记。

第一个是self-assignment测试。 这里检查有两个用途: 这是一个简单的方法来防止我们在self-assignment上运行不必要的代码,并且它保护我们免受微妙的Bug ( 如删除数组以便尝试复制它) 。 但在所有其他情况下,它仅仅是减慢程序的速度,并在代码中充当噪音;self-assignment很少发生,所以大多数时候这种检查是浪费。 如果操作员没有它就能正常工作会更好。

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


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

代码已经展开这将导致第三个问题: ! 代码复制。我们的赋值运算符有效地重复了我们在别处编写的所有代码,这是一个可怕的事情。

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

( 一个可能会奇怪:如果需要这么多的代码来正确管理一个资源,那么如果我的类管理不止一个? 虽然这似乎是一个有效的关注点,但实际上它需要 non-trivial try/catch 子句,这是一个 non-issue 。 那是因为某类应管理 一个资源只有 ) !

一个成功的解决方案

就像前面提到的,copy-and-swap习惯用法将解决所有这些问题。 但是现在,除了一个以外,我们还有所有需求: 一个 swap 函数。当三个规则成功地需要我们的copy-constructor,赋值操作符和析构函数时,它应该被称为"大的三和半": 当你的类管理一个资源时,提供一个 swap 函数也有意义。


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

//...
};

现在我们不仅可以交换我们的dumb_array,而且一般交换可以更加高效;它只是交换指针和大小,而不是分配和复制整个数组。 除了在功能和效率方面的奖励之外,我们现在可以实现copy-and-swap习惯用法了。

没有进一步的ado,我们的赋值运算符是:


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中也是至关重要的,它在后面讨论。 ( 在一般情况下,一个非常有用的指南如下:

不管怎样,获取资源的方法是消除代码重复的关键: 我们可以使用来自copy-constructor的代码来制作副本,而无需重复任何一点。 现在复制已经完成,我们准备交换。

请注意,在输入所有新数据已经分配,复制和准备使用的函数时。 这为我们提供了免费的异常保证: 如果复制失败,我们甚至无法输入函数,因此不能改变 *this的状态。 ( 我们为一个强大的异常保证而手工做了些什么,编译器现在为我们做了些什么;)

现在我们是 home-free,因为 swap 是 non-throwing 。 我们用复制的数据交换当前的数据,安全地改变我们的状态,旧的数据被放入临时数据。 当函数返回时,将释放旧数据。 ( 在参数结束范围及其析构函数被调用时)

因为该习惯用法不重复代码,所以我们不能在运算符中引入 Bug 。 注意,这意味着我们不需要self-assignment检查,允许单一的operator= 实现。 ( 另外,我们在non-self-assignments上不再有性能损失。)

这是copy-and-swap的习惯用法。

C++11?

C++的下一个版本C++11对我们如何管理资源做出了非常重要的更改: 三是现在的规律的规律四 ( 一个半半) 。 为什么因为我们不仅需要能够copy-construct资源,还需要move-construct它。

幸运的是,这很简单:


class dumb_array
{
public:
//...

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

//...
};

这里发生了什么撤回move-construction的目标:? 从类的另一个实例获取资源,使它的处于可以分配和destructible状态。

我们所做的很简单: 之后通过默认构造函数( C++11特性) swapping,初始化,然后跟我们班 other ;我们知道一个默认创建的类实例可以安全地交换被分配和析构,这样我们才能知道 other 将能够做同样的,.

( 注意,有些编译器不支持构造函数委托;在本例中,我们必须手动构造类。 这是一个不幸但幸运的任务。

那为什么工作?

这是我们需要对我们的类进行的唯一更改,所以它为什么工作? 记住我们做出的使参数成为一个值而不是引用的ever-important决策:


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

现在,如果 other 正被初始化为一个右值,它将被 move-constructed 。 完美的。用同样的方法C++03我们让我们 re-use copy-constructor功能通过采用该参数 by-value,C++11可以自动 选出最move-constructor也在适当的时候。 ( 当然,就像前面链接的文章中提到的,值的复制/移动可以完全省略) 。

所以copy-and-swap的习惯用法。


脚注

*Why 是否将 mArray 设置为 null? 因为如果操作符中的任何进一步代码抛出,dumb_array的析构函数可能会被调用;如果没有将它设置为空,我们尝试删除已经删除的内存 ! 我们通过将它设置为null来避免,因为删除null是一个 no-operation 。

任何正确使用 swap 都将通过一个非限定调用,我们的功能将通过 ADL 。 一个函数将执行。

通过在参数列表中进行复制,你可以最大化优化。

工作分配是两个步骤: 拆除旧的对象状态建筑的新状态以副本方式的某些其他对象的状态。

基本上那就是那个 析构函数复制构造函数 做的,所以对他们的第一想法是在将这里工作。 不过,由于析构一定不能失败,而施工可能,我们实际上希望先采用其他的方式围绕,比如 的表义性第一次演出部分,以及这种成功,并再次对破坏性部分 。 copy-and-swap习惯用法是一种方法: 它首先调用一个类'复制构造函数to来创建临时,然后用临时的方法交换它的数据,然后让临时销毁的析构函数销毁旧的状态。
因为 swap() 应该永远不会失败,所以可能失败的唯一部分是 copy-construction 。 这是先执行的,如果失败,目标对象中将不会更改任何内容。

在它的精化形式中,copy-and-swap通过初始化赋值运算符的( non-reference ) 参数来实现:


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

这个答案更像是加法,对上面的答案稍加修改。

在 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


毕竟,你不需要来使用一个对象来交换 2 friend 函数。 使 swap 成为一个具有一个 other 对象作为参数的成员函数变得非常合理。

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

已经有一些好的答案了。 随着copy-and-swap习语。., 我将集中主要的我认为他们缺乏-的说明,上

copy-and-swap用法是什么?

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


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

基本思想是:

  • 分配给对象的最error-prone部分是确保获取新状态所需的任何资源( 例如。 内存,描述符

  • 企业购入才能尝试 之前修改的当前状态对象( 例如 。 *this ) 如果寄去新的价值,这就是为什么 rhs 是接受后依据价值 ( 例如 。 按引用复制) 而不是

  • 在> = C++11 ),交换本地副本的状态通常 rhs*this 是 无潜在故障/异常,给定本地副本相对容易一些,不需要什么特别的状态以后( 就需要进行技术状态适合于要运行的析构函数,当正在移动为对象从

应该什么时候使用它? ( 这问题它是否解决 [/create])?

  • 当你想要一个干净的,易于理解的,健壮的方式来定义赋值操作符的( 比较简单) 复制构造函数,swap 和析构函数函数。

  • 当在工作分配过程中使用额外临时对象创建的任何性能损失或者更高的资源占用时,对于你的应用程序来说并不重要。

X tmp = lhs; lhs = rhs; rhs = tmp; 而copy-construction或者赋值可能抛出,仍然有可能导致一些数据成员交换,而其他数据成员没有交换。 这个潜在的应用也适用于 C++03 std::string,如James对另一个答案的评论:

@wilhelmtell: C++03中,抛出的异常有可能的,但是没有说明 std::: 交换( 由 std:: swap调用) 内容时调度。 在C++0x中,std:: 字符串:: swap是 noexcept,不能引发异常。 - James McNellis Dec 22'10 at 15: 24


尽管看上去不堪设想,客户端代码甚至会尝试self-assignment上过程中,会发生读取相对易于算法。操作容器,用宏 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_);
 }
. . .
};

self-assignment上,上面的代码在一个新分配的堆区域,然后删除,点 p_x.p_;,尝试读取该 uninitialised 数据这其中( 未定义的行为) 着,如果这些都不做任何事情太怪异,copy 尝试一个self-assignment为每一 just-destructed't'!



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

在这里,hand-written Client::operator= 可能检查 *this 是否已经与 rhs ( 也许发送一个"重置"如果有用则代码) 连接,而copy-and-swap方法将调用 copy-constructor,它可能会被写入以打开一个不同的套接字连接,然后关闭原来的连接。 这不仅意味着远程网络交互而不是简单的in-process变量复制,它可以在套接字资源或者连接上运行客户端或者服务器限制的afoul 。 ( 当然这个类有一个可怕的接口,但这是另一个问题;-P ) 。

当你处理 C++11-style allocator-aware容器时,我想添加一个警告。 交换和赋值有细微不同的语义。

为了具体化,我们考虑一个容器 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 最初的状态。 然而,有一个隐藏的问题: 如果发生了什么 a.get_allocator()!= b.get_allocator() 答案是:它取决于。 让我们写吧 AT = std::allocator_traits<A>

  • if AT::propagate_on_container_move_assignment 然后是否 std::true_typefmb.get_allocator()的值,否则它不会重新指派 A的分配器和 A 继续使用它的原有的分配器。 在这种情况下,需要单独交换数据元素,因为 Ab的存储不兼容。

  • 如果 AT::propagate_on_container_swapstd::true_type,那么 fs 以期望的方式交换数据和分配器。

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

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

结果造成已经交换技术已经成为non-trivial操作于C++11只要你的容器启动时支持有状态的分配器。 这有点"高级用例",但这并不是完全不可能的,因为移动优化通常只在你的类管理一个资源时变得有趣,内存是最流行的资源之一。

...