什么是复制和交换成语?
这个成语是什么,应该在什么时候使用?它解决了哪些问题?使用 C++11 时习语会发生变化吗?
What is this idiom and when should it be used? Which problems does it solve? Does the idiom change when C++11 is used?
虽然在很多地方都提到过,但我们没有任何单数的它是什么"问题和答案,所以在这里.以下是之前提到的部分地点列表:
Although it's been mentioned in many places, we didn't have any singular "what is it" question and answer, so here it is. Here is a partial list of places where it was previously mentioned:
- 你最喜欢的 C++ 编码风格成语是什么:复制交换
- 复制C++ 中的构造函数和 = 运算符重载:通用函数是否可行?
- 什么是复制省略和它如何优化复制和交换习语
- C++:动态分配对象数组?一个>
推荐答案
概览
为什么我们需要复制和交换习语?
任何管理资源的类(包装器,如智能指针)都需要实现 三巨头.虽然复制构造函数和析构函数的目标和实现很简单,但复制赋值运算符可以说是最细微和最困难的.应该怎么做?需要避免哪些陷阱?
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?
copy-and-swap idiom 是解决方案,它优雅地帮助赋值运算符实现两件事:避免 代码重复,并提供强大的异常保证一个>.
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.
概念上,它通过使用copy-constructor 的功能是创建数据的本地副本,然后使用 swap
函数获取复制的数据,将旧数据与新数据交换.然后临时副本销毁,并带走旧数据.我们留下了一份新数据的副本.
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.
为了使用 copy-and-swap 习惯用法,我们需要三样东西:一个工作的复制构造函数,一个工作的析构函数(两者都是任何包装器的基础,所以无论如何都应该是完整的),以及一个 交换
函数.
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.
交换函数是一个非抛出函数,它交换一个类的两个对象,成员对成员.我们可能会想使用 std::swap
而不是自己提供,但这是不可能的;std::swap
在其实现中使用复制构造函数和复制赋值运算符,我们最终会尝试根据自身定义赋值运算符!
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!
(不仅如此,对 swap
的非限定调用将使用我们的自定义交换运算符,跳过 std::swap
会不必要地构造和销毁我们的类需要.)
(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.)
让我们考虑一个具体的案例.我们想在一个无用的类中管理一个动态数组.我们从一个有效的构造函数、复制构造函数和析构函数开始:
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;
};
这个类几乎成功地管理了数组,但是它需要operator=
才能正常工作.
This class almost manages the array successfully, but it needs operator=
to work correctly.
这是一个简单的实现可能看起来如何:
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;
}
我们说我们已经完成了;这现在管理一个数组,没有泄漏.但是,它存在三个问题,在代码中按顺序标记为 (n)
.
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)
.
首先是自测.
这个检查有两个目的:它是一种防止我们在自赋值时运行不必要的代码的简单方法,它可以保护我们免受细微的错误(例如删除数组只是为了尝试复制它).但在所有其他情况下,它只会降低程序的速度,并在代码中充当噪音;自赋值很少发生,所以大多数时候这种检查是浪费.
如果操作员没有它也能正常工作就更好了.
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.
第二个是它只提供了一个基本的异常保证.如果 new int[mSize]
失败,则 *this
将被修改.(即大小不对,数据没了!)
对于强大的异常保证,它需要类似于:
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;
}
代码已扩展!这就引出了第三个问题:代码重复.
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.
(可能有人会问:如果要正确管理一个资源需要这么多代码,如果我的班级管理多个资源怎么办?
虽然这似乎是一个有效的问题,并且确实需要非平凡的 try
/catch
子句,但这不是问题.
这是因为一个类应该管理一个资源!)
(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!)
如前所述,复制和交换习语将解决所有这些问题.但是现在,除了一个 swap
函数之外,我们还有所有的要求.虽然三法则成功地包含了我们的复制构造函数、赋值运算符和析构函数,但它确实应该被称为三巨头半":任何时候你的类管理资源时,提供一个swap
函数.
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);
}
// ...
};
(这里是公开好友交换
.) 现在我们不仅可以交换我们的 dumb_array
,而且交换通常会更有效;它只是交换指针和大小,而不是分配和复制整个数组.除了功能和效率方面的优势之外,我们现在已经准备好实现复制和交换习惯用法了.
(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.
我们首先注意到一个重要的选择:参数参数是按值.虽然人们可以很容易地做到以下几点(事实上,这个习语的许多幼稚的实现都是如此):
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;
}
我们失去了一个 重要的优化机会.不仅如此,这种选择在 C++11 中也很关键,后面会讨论.(一般来说,一个非常有用的指导方针如下:如果您要在函数中复制某些内容,请让编译器在参数列表中完成它.?)
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.
请注意,在进入函数时,所有新数据都已分配、复制并准备好使用.这就是免费为我们提供强大的异常保证的原因:如果副本的构造失败,我们甚至不会进入函数,因此不可能更改 *this
的状态.(我们之前手动为强异常保证所做的事情,现在编译器正在为我们做事情;怎么样.)
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.)
此时我们是无家可归的,因为 swap
是不抛出的.我们将当前数据与复制的数据交换,安全地更改我们的状态,并将旧数据放入临时数据中.当函数返回时,旧数据被释放.(参数的作用域结束并调用其析构函数.)
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.)
因为成语没有重复代码,所以我们不能在操作符中引入错误.请注意,这意味着我们不再需要进行自分配检查,从而允许 operator=
的单一统一实现.(此外,我们不再对非自我分配产生性能损失.)
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.
C++ 的下一个版本,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.
幸运的是,这很容易:
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.
所以我们所做的很简单:通过默认构造函数(C++11 的一个特性)进行初始化,然后与 other
进行交换;我们知道可以安全地分配和销毁我们类的默认构造实例,因此我们知道 other
将能够在交换后做同样的事情.
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.)
这是我们需要对类进行的唯一更改,为什么它会起作用?请记住我们做出的让参数成为值而不是引用的重要决定:
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)
现在,如果 other
正在用右值初始化,它将被移动构造.完美的.与 C++03 让我们通过按值获取参数来重用复制构造函数功能的方式相同,C++11 也会在适当的时候自动选择移动构造函数.(当然,正如之前链接的文章中提到的,值的复制/移动可能会被完全省略.)
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.
*为什么我们将 mArray
设置为 null?因为如果运算符中的任何进一步代码抛出,可能会调用 dumb_array
的析构函数;如果在没有将其设置为 null 的情况下发生这种情况,我们将尝试删除已被删除的内存!我们通过将其设置为 null 来避免这种情况,因为删除 null 是无操作的.
*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.
?还有其他主张我们应该为我们的类型专门化 std::swap
,提供一个类内 swap
以及一个自由函数 swap
等.但这都是不必要的:任何对 swap
的正确使用都将通过非限定调用,而我们的函数将通过 ADL.一个函数就可以了.
?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.
?原因很简单:一旦您拥有了自己的资源,您就可以在任何需要的地方交换和/或移动它 (C++11).通过在参数列表中进行复制,您可以最大限度地优化.
?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.
??移动构造函数一般应该是noexcept
,否则一些代码(例如std::vector
调整大小的逻辑)即使移动会使用复制构造函数感觉.当然,只有在里面的代码没有抛出异常的情况下才标记为noexcept.
??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.
相关文章