What is the copy-and-swap idiom?
注:红色括号为自己的注解,括号中没有翻译的是作者的注释比较简单直接引用,专有的词语直接引用。能力有限欢迎大家指正。原文地址:http://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom
Overview
Why do we need it?
任何一个类在操纵资源时都需要实现“The Big Three”。尽管实现copy-constructor 以及destructor十分的直接,但是copy-assignment operator的实现却不是那么简单。那么我们如何来实现copy-assignment operator,又需要避免哪些呢?这时我们所说的Copy-and-swap 就成功的帮助我们实现了两样:1,避免代码冗余。2, 提供了强的异常控制
How does it work?
我们的copy-and-swap通过copy-constructor为我们创建一个局部变量,再通过swap函数将原始数据与新的数据(我们创建的局部变量)进行交换,在swap函数退出时调用destructor 将原始数据析构掉,最后我们就得到了一份新数据拷贝。为了使用copy-and-swap,我们需要一个可工作的copy constructor,destructor(both are the basis of any wrapper, so should be complete anyway)以及一个swap函数。所谓swap函数的功能是交换一个类的两个对象,并且不抛出异常。我们可能会想到std::swap,但是swap函数需要调用我们自己的operator=,这是不可能的。所以我们需要重新定义我们的swap函数。(如果我们直接使用将会出现自己调用自己,就像你将copy-constructor的引用符去掉一样会陷入死循环)。Std::swap的vc下的源码。
template<class _Ty> inline
void swap(_Ty& _X, _Ty& _Y)
{_Ty _Tmp = _X;
_X = _Y, _Y = _Tmp; }
注:上述中的 The Big Three指的是copy构造函数,赋值运算符,以及析构函数具体可参考(http://stackoverflow.com/questions/4172722/what-is-the-rule-of-three)
An in-depth explanation
我们考虑一个实际的例子,我们想要实现一个动态的数组。我们为其实现了一个构造函数,copy构造函数,以及一个析构函数。
#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;
};This class almost manages the
这个类成功的实现了上述动态数组,但是我们还需要一个operator=使其正常运行。
A failed solution
下面给出一个天真的实现方式
// 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,它很容的阻止我们在self-assignment中运行一些不必要的代码。2,使我们免遭一些微妙的bug(such as deleting the array only to try and copy it),但是事实上它很少降低我们的运行速度,反而像一些noise(不知如何翻译,所以直接引用);因为自我赋值很少发生,所以大部分的检测都是无用的,因此程序没有它可能会更好。
第二点是它仅仅为我们提供了一个基本的异常控制。如果 new int[mSize]失败,*this将会被修改(Namely,the size is wrong and the data is gone!)当我们需要确保一个较强的异常控制时,代码可能是如下所示;
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;
}
代码量彭胀!这个导致第三个问题的出现:代码冗余。我们的operator=重复了我们的代码,这个太恐怖了。其实在我们的例子中,核心代码只有两行(the allocation and the copy),但是对于更复杂的资源操纵可能会使我们的代码急剧的彭胀,因此我们应该努力的避免重复。
(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
正如我们我们上面提到的,copy-and-swap将会解决上述问题。但是现在我们所有的东西都已准备就绪就差一个swap函数。尽管 “The Rule of Three”(专用术语) 成功的叙述了copy-constructor,assignment operator,以及destructor存在性,但是其真正应该被称为“The Big Three and A Half”。任何时候你的类在操纵资源时都应该提供一个swap函数。因此我们也需要为我们添加一个自己的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);
}
// ...
};
现在swap交换了我们的dumb_array,它仅仅交换指针和大小,而不是分配以及拷贝整个对象。抛开其功能以及效率,我们现在已经准备好实现我们的copy-and-swap idiom(个人认为将swap定义为成员函数更好)。不需要更多的赘述我们的赋值运算符实现如下:
dumb_array& operator=(dumb_array other) // (1)
{
swap(*this, other); // (2)
return *this;
}
Why does it work?
我们首先有一个很重要的发现:我们的参数是以传值的方式引入的(注:在Effective C++中,要求对与内置参数尽量已传值的方式传参,对与构造参数尽量以引用的方式传值,但是在此传值是因为我们不想更改传入参数的状态,我们更改的只是原值的一个副本)。当然我们可以下述方式实现(and indeed many naive implementation of the idiom do):
dumb_array& operator=(const dumb_array& other)
{
dumb_array temp(other);
swap(*this, temp);
return *this;
}
无论哪种方法,这种获取资源的方法是剔除代码重复的关键,因为我们尝试着运用copy-construct 中的代码来获取一份拷贝并且没有任何额外重复。现在我们有了一份拷贝,那么我们就可以进行交换了。从上面函数我们为新数据分配了内存空间,一份拷贝并且我们已经准备好了交换。上述这些操作为我们提供免费提供了异常控制(例如new失败)。如果构造对象失败,我们也不可能更改*this的对象的状态(what we did manually before for a strong exception guarantee , the compiler is doing for us now; how kind)
我们将当前的数据与拷贝的数据交换,安全的更改了我们的状态(指*this)。旧的数据(指的是前面的当前数据)当函数退出时被释放掉(where opon the parameter’s scope ends its destructor is called)很高兴我们引入了一个operator = 的实现,但是我们没有在赋值操作符中引入bug而且避免了self-assignment的检查,that’s great!(Additionally,we no longer have a performance penalty on non-self-assignments)这就是“copy-and-assignment”。