C++ 复制和交换的习惯用法是什么?
这个成语是什么?什么时候用?它解决了哪些问题?当使用C++11时,习惯用法会改变吗 虽然在很多地方都提到过它,但我们没有任何单一的“它是什么”问题和答案,所以就在这里。以下是之前提到的部分地点列表:C++ 复制和交换的习惯用法是什么?,c++,copy-constructor,assignment-operator,c++-faq,copy-and-swap,C++,Copy Constructor,Assignment Operator,C++ Faq,Copy And Swap,这个成语是什么?什么时候用?它解决了哪些问题?当使用C++11时,习惯用法会改变吗 虽然在很多地方都提到过它,但我们没有任何单一的“它是什么”问题和答案,所以就在这里。以下是之前提到的部分地点列表: 概述 为什么我们需要复制和交换习惯用法? 任何管理资源的类(包装器,如智能指针)都需要实现。虽然复制构造函数和析构函数的目标和实现很简单,但复制赋值操作符可以说是最微妙和最困难的。应该怎么做?需要避免哪些陷阱 复制和交换习惯用法就是解决方案,它优雅地帮助赋值操作符实现两件事:避免和提供一个
swap
函数获取复制的数据,用新数据交换旧数据。然后,临时副本将销毁,并带走旧数据。我们只剩下一份新数据的副本
为了使用复制和交换习惯用法,我们需要三样东西:工作的复制构造函数、工作的析构函数(两者都是任何包装器的基础,因此无论如何都应该是完整的)和交换函数
交换函数是一个非抛出函数,它将一个类的两个对象(成员对成员)交换。我们可能会尝试使用std::swap
而不是提供我们自己的,但这是不可能的<代码>标准::交换
在其实现中使用复制构造函数和复制赋值运算符,我们最终将尝试根据赋值运算符本身定义赋值运算符
(不仅如此,对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;
};
我们说我们完了;现在,它可以管理阵列,而不会出现泄漏。但是,它有三个问题,在代码中顺序标记为(n)
第一个是自我分配测试。
此检查有两个目的:它是一种简单的方法,可以防止我们在自我分配时运行不必要的代码,并且可以保护我们免受细微的错误(例如删除数组只是为了尝试复制它)。但在所有其他情况下,它只会减慢程序的速度,并在代码中起到噪音的作用;自我分配很少发生,因此大多数情况下,此检查是一种浪费。
如果操作员没有它也能正常工作,那就更好了
第二,它只提供了基本的例外保证。如果new int[mSize]
失败,*此
将被修改。(即大小错误,数据丢失!)
对于强有力的例外保证,它需要类似于:
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;
}
代码已经扩展!这就引出了第三个问题:代码重复
我们的赋值运算符有效地复制了我们在别处已经编写的所有代码,这是一件可怕的事情
在我们的例子中,它的核心只有两行(分配和拷贝),但是对于更复杂的资源,这段代码膨胀可能是相当麻烦的。我们应该努力做到永不重复
(有人可能会想:如果正确管理一个资源需要这么多代码,那么如果我的类管理多个资源呢?
虽然这似乎是一个值得关注的问题,而且确实需要非常重要的try
/catch
子句,但这不是一个问题。
这是因为类应该管理!)
成功的解决方案
如上所述,复制和交换习惯用法将解决所有这些问题。但是现在,除了一个要求外,我们有所有的要求:一个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 objects,
// the two objects are effectively swapped
swap(first.mSize, second.mSize);
swap(first.mArray, second.mArray);
}
// ...
};
(这就是为什么公共朋友交换
)现在我们不仅可以交换我们的dumb_数组
,而且交换通常可以更有效;它只是交换指针和大小,而不是分配和复制整个数组。除了在功能性和效率方面的优势之外,我们现在已经准备好实现复制和交换习惯用法
无需进一步说明,我们的赋值运算符为:
dumb_array& operator=(dumb_array other) // (1)
{
swap(*this, other); // (2)
return *this;
}
就这样!一举,所有三个问题都能优雅地同时解决
它为什么有效?
我们首先注意到一个重要的选择:参数参数按值取值。虽然我们可以很容易地做到以下几点(事实上,许多简单的习语实现都可以做到):
我们输了一场比赛。不仅如此,这个选择在C++11中也是至关重要的,后面将讨论这个问题。(一般来说,下面是一条非常有用的指导原则:如果要在函数中复制某些内容,请让编译器在参数列表中执行此操作。”——)
无论哪种方式,这种获取资源的方法都是消除代码重复的关键:我们可以使用复制构造函数中的代码进行复制,而不需要重复任何一点。既然复制完成了,我们就可以交换了
请注意,进入该功能后,所有新数据都已分配、复制并准备好使用。这给了我们一个强有力的免费例外保证:我们不会
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;
}
class dumb_array
{
public:
// ...
// move constructor
dumb_array(dumb_array&& other) noexcept ††
: dumb_array() // initialize via default constructor, C++11 only
{
swap(*this, other);
}
// ...
};
dumb_array& operator=(dumb_array other); // (1)
T& operator=(T tmp)
{
this->swap(tmp);
return *this;
}
friend void swap(A& first, A& second) {
std::swap(first.size, second.size);
std::swap(first.arr, second.arr);
}
void swap(A& other) {
std::swap(size, other.size);
std::swap(arr, other.arr);
}
X& operator=(X rhs)
{
swap(rhs);
return *this;
}
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_);
}
...
};
struct Client
{
IP_Address ip_address_;
int socket_;
X(const X& rhs)
: ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
{ }
};
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);
}