C++ 什么是复制和交换习语?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/3279543/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
What is the copy-and-swap idiom?
提问by GManNickG
What is this idiom and when should it be used? Which problems does it solve? Does the idiom change when C++11 is used?
这个成语是什么,什么时候应该使用?它解决了哪些问题?使用 C++11 时习语会改变吗?
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:
虽然在很多地方都提到过,但我们没有任何单一的“它是什么”的问题和答案,所以就在这里。这是之前提到的地方的部分列表:
回答by GManNickG
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?
任何管理资源的类(包装器,如智能指针)都需要实现三巨头。虽然复制构造函数和析构函数的目标和实现很简单,但复制赋值运算符可以说是最微妙和最困难的。应该怎么做?需要避免哪些陷阱?
The copy-and-swap idiomis the solution, and elegantly assists the assignment operator in achieving two things: avoiding code duplication, and providing a strong exception guarantee.
在复制和交换成语是解决方案,并协助典雅赋值运算符在实现两件事情:避免重复代码,并提供了一个强大的异常保证。
How does it work?
它是如何工作的?
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.
从概念上讲,它通过使用复制构造函数的功能来创建数据的本地副本,然后使用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.
为了使用复制和交换习语,我们需要三样东西:一个有效的复制构造函数、一个有效的析构函数(两者都是任何包装器的基础,所以无论如何都应该是完整的)和一个swap
函数。
A swap function is a non-throwingfunction 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!
交换函数是一个非抛出函数,它交换一个类的两个对象,成员对成员。我们可能会倾向于使用std::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.)
(不仅如此,对 的不合格调用swap
将使用我们的自定义交换运算符,跳过std::swap
可能需要对我们的类进行不必要的构造和销毁。)
An in-depth explanation
深入的解释
The goal
目标
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;
};
This class almost manages the array successfully, but it needs operator=
to work correctly.
这个类几乎成功地管理了数组,但它需要operator=
正常工作。
A failed solution
失败的解决方案
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;
}
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)
.
我们说我们完成了;这现在管理一个数组,没有泄漏。但是,它存在三个问题,在代码中按顺序标记为(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.
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.
首先是自我分配测试。这种检查有两个目的:它是一种防止我们在自赋值时运行不必要代码的简单方法,它可以保护我们免受细微的错误(例如删除数组只是为了尝试和复制它)。但在所有其他情况下,它只会减慢程序速度,并在代码中充当噪音;自赋值很少发生,所以大部分时间这个检查都是浪费。如果操作员没有它也能正常工作就更好了。
第二个是它只提供基本的异常保证。如果
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]() : 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; }
代码已扩展!这就引出了第三个问题:代码重复。我们的赋值运算符有效地复制了我们已经在别处编写的所有代码,这是一件可怕的事情。
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.
在我们的例子中,它的核心只有两行(分配和复制),但是对于更复杂的资源,这个代码膨胀可能会很麻烦。我们应该努力不再重蹈覆辙。
(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!)
(有人可能想知道:如果正确管理一个资源需要这么多代码,如果我的类管理多个资源怎么办?虽然这似乎是一个有效的问题,而且确实需要非平凡的try
/catch
子句,但这是一个非-issue。那是因为一个类应该只管理一个资源!)
A successful solution
一个成功的解决方案
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.
如前所述,复制和交换习语将解决所有这些问题。但是现在,我们有所有的需求,除了一个:一个swap
函数。虽然三定律成功地包含了我们的复制构造函数、赋值运算符和析构函数,但它真的应该被称为“三大半”:任何时候你的类管理一个资源,提供一个swap
函数也是有意义的.
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);
}
// ...
};
(Hereis 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.
(这是原因的解释public friend swap
。)现在我们不仅可以交换我们dumb_array
的 ,而且交换通常可以更有效;它只是交换指针和大小,而不是分配和复制整个数组。除了功能和效率方面的这一优势外,我们现在准备实施复制和交换习语。
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.
就是这样!一举一动,所有三个问题都得到了优雅的解决。
Why does it work?
为什么有效?
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;
}
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.?)
我们失去了一个重要的优化机会。不仅如此,这个选择在 C++11 中也很关键,后面会讨论。(一般来说,一个非常有用的指南如下:如果您要在函数中复制某些内容,请让编译器在参数列表中进行复制。?)
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.
无论哪种方式,这种获取资源的方法都是消除代码重复的关键:我们可以使用复制构造函数中的代码进行复制,而无需重复任何部分。现在副本已经制作完成,我们准备好交换了。
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.)
观察到,在进入函数时,所有新数据都已分配、复制并准备好使用。这就是为我们提供强大的免费异常保证的原因:如果复制的构造失败,我们甚至不会进入该函数,因此不可能更改*this
. (我们之前手动为强大的异常保证所做的,编译器现在正在为我们做;怎么样。)
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.)
此时我们无家可归,因为swap
是非投掷。我们用复制的数据交换当前数据,安全地改变我们的状态,旧数据被放入临时数据。然后在函数返回时释放旧数据。(在参数的范围结束并调用其析构函数时。)
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.)
因为习语没有重复代码,所以我们不能在操作符中引入错误。请注意,这意味着我们不再需要自赋值检查,从而允许operator=
. (此外,我们不再对非自我分配有性能损失。)
And that is the copy-and-swap idiom.
这就是复制和交换习语。
What about C++11?
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.
C++ 的下一个版本,C++11,对我们管理资源的方式进行了一个非常重要的改变:三的规则现在是四的规则(半)。为什么?因为我们不仅需要能够复制构造我们的资源,我们还需要移动构造它。
Luckily for us, this is easy:
幸运的是,这很容易:
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.
这里发生了什么?回想一下移动构造的目标:从类的另一个实例中获取资源,使其处于保证可分配和可破坏的状态。
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.
所以我们所做的很简单:通过默认构造函数(C++11 特性)初始化,然后用other
;交换。我们知道我们类的默认构造实例可以安全地分配和销毁,因此我们知道other
在交换后也可以这样做。
(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.)
(请注意,有些编译器不支持构造函数委托;在这种情况下,我们必须手动默认构造类。这是一项不幸但幸运的微不足道的任务。)
Why does that work?
为什么这样做?
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)
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 automaticallypick 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.)
现在,如果other
使用 rvalue 初始化,它将是 move-constructed。完美的。与 C++03 让我们通过按值获取参数来重用我们的复制构造函数功能一样,C++11 也会在适当的时候自动选择移动构造函数。(当然,正如之前链接的文章中提到的,值的复制/移动可能会被完全省略。)
And so concludes the copy-and-swap idiom.
复制和交换习语到此结束。
Footnotes
脚注
*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.
*为什么我们设置mArray
为null?因为如果运算符中的任何其他代码抛出,dumb_array
可能会调用析构函数;如果发生这种情况而未将其设置为 null,我们将尝试删除已删除的内存!我们通过将其设置为 null 来避免这种情况,因为删除 null 是一种无操作。
?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.
? 还有其他主张我们应该专门std::swap
针对我们的类型,在类中提供swap
一个自由函数swap
,等等。但这都是不必要的:任何正确使用swap
will 都是通过不合格的调用,我们的函数将是通过ADL找到。一个功能就行。
?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.
?原因很简单:一旦您拥有自己的资源,您就可以将其交换和/或移动 (C++11) 到任何需要的地方。通过在参数列表中进行复制,您可以最大限度地优化。
??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.
??移动构造函数通常应该是noexcept
,否则std::vector
即使移动有意义,某些代码(例如调整大小逻辑)也会使用复制构造函数。当然,只有在里面的代码没有抛出异常的情况下才标记为noexcept。
回答by sbi
Assignment, at its heart, is two steps: tearing down the object's old stateand building its new state as a copyof some other object's state.
赋值的核心是两个步骤:拆除对象的旧状态并构建其新状态作为其他对象状态的副本。
Basically, that's what the destructorand the copy constructordo, so the first idea would be to delegate the work to them. However, since destruction mustn't fail, while construction might, we actually want to do it the other way around: first perform the constructive partand, if that succeeded, then do the destructive part. The copy-and-swap idiom is a way to do just that: It first calls a class' copy constructor to create a temporary object, then swaps its data with the temporary's, and then lets the temporary's destructor destroy the old state.
Since swap()
is supposed to never fail, the only part which might fail is the copy-construction. That is performed first, and if it fails, nothing will be changed in the targeted object.
基本上,这就是析构函数和复制构造函数所做的,所以第一个想法是将工作委托给它们。但是,由于破坏不能失败,而构建可能会失败,我们实际上想以相反的方式进行:首先执行建设性部分,如果成功,则执行破坏性部分。copy-and-swap 惯用语就是这样做的一种方法:它首先调用类的复制构造函数来创建一个临时对象,然后将其数据与临时对象交换,然后让临时对象的析构函数破坏旧状态。
自从swap()
应该永远不会失败,唯一可能失败的部分是复制构造。这是首先执行的,如果失败,则目标对象中的任何内容都不会更改。
In its refined form, copy-and-swap is implemented by having the copy performed by initializing the (non-reference) parameter of the assignment operator:
在其改进的形式中,复制和交换是通过初始化赋值运算符的(非引用)参数来执行复制来实现的:
T& operator=(T tmp)
{
this->swap(tmp);
return *this;
}
回答by Tony Delroy
There are some good answers already. I'll focus mainlyon what I think they lack - an explanation of the "cons" with the copy-and-swap idiom....
已经有一些很好的答案。我将主要关注我认为他们缺乏的东西 - 用复制和交换习语解释“缺点”......
What is the copy-and-swap idiom?
什么是复制和交换习语?
A way of implementing the assignment operator in terms of a swap function:
一种根据交换函数实现赋值运算符的方法:
X& operator=(X rhs)
{
swap(rhs);
return *this;
}
The fundamental idea is that:
基本思想是:
the most error-prone part of assigning to an object is ensuring any resources the new state needs are acquired (e.g. memory, descriptors)
that acquisition can be attempted beforemodifying the current state of the object (i.e.
*this
) if a copy of the new value is made, which is whyrhs
is accepted by value(i.e. copied) rather than by referenceswapping the state of the local copy
rhs
and*this
is usuallyrelatively easy to do without potential failure/exceptions, given the local copy doesn't need any particular state afterwards (just needs state fit for the destructor to run, much as for an object being movedfrom in >= C++11)
分配给对象最容易出错的部分是确保获取新状态所需的任何资源(例如内存、描述符)
该采集可以尝试之前修改该对象(即,当前状态
*this
,如果新的值的副本。制造),这就是为什么rhs
被接受由值(即复制的),而不是通过引用交换本地副本的状态
rhs
和*this
是通常比较容易做到无潜在故障/异常,考虑到本地副本不需要任何特殊的状态之后(只需要状态适合析构函数运行,就像一个对象正在移动来自 >= C++11)
When should it be used? (Which problems does it solve [/create]?)
应该什么时候使用?(它解决了哪些问题[/create]?)
When you want the assigned-to objected unaffected by an assignment that throws an exception, assuming you have or can write a
swap
with strong exception guarantee, and ideally one that can't fail/throw
..?When you want a clean, easy to understand, robust way to define the assignment operator in terms of (simpler) copy constructor,
swap
and destructor functions.- Self-assignment done as a copy-and-swap avoids oft-overlooked edge cases.?
- When any performance penalty or momentarily higher resource usage created by having an extra temporary object during the assignment is not important to your application. ?
当您希望被分配对象不受引发异常的赋值的影响时,假设您有或可以编写一个
swap
具有强异常保证的对象,并且理想情况下不会失败/throw
..?当您想要一种干净、易于理解、健壮的方式来根据(更简单的)复制构造
swap
函数和析构函数定义赋值运算符时。- 作为复制和交换完成的自分配避免了经常被忽视的边缘情况。?
- 当在分配期间通过额外的临时对象创建的任何性能损失或暂时更高的资源使用对您的应用程序并不重要时。?
? swap
throwing: it's generally possible to reliably swap data members that the objects track by pointer, but non-pointer data members that don't have a throw-free swap, or for which swapping has to be implemented as X tmp = lhs; lhs = rhs; rhs = tmp;
and copy-construction or assignment may throw, still have the potential to fail leaving some data members swapped and others not. This potential applies even to C++03 std::string
's as James comments on another answer:
? swap
抛出:通常可以可靠地交换对象通过指针跟踪的数据成员,但没有无抛出交换的非指针数据成员,或者必须实现交换的非指针数据成员,X tmp = lhs; lhs = rhs; rhs = tmp;
并且复制构造或赋值可能throw,仍然有可能失败,让一些数据成员被交换,而另一些则不会。这种潜力甚至适用于 C++03 std::string
,正如詹姆斯对另一个答案的评论:
@wilhelmtell: In C++03, there is no mention of exceptions potentially thrown by std::string::swap (which is called by std::swap). In C++0x, std::string::swap is noexcept and must not throw exceptions. – James McNellis Dec 22 '10 at 15:24
@wilhelmtell:在 C++03 中,没有提到 std::string::swap(由 std::swap 调用)可能抛出的异常。在 C++0x 中,std::string::swap 是 noexcept 并且不能抛出异常。– 詹姆斯·麦克内利斯 10 年 12 月 22 日 15:24
? assignment operator implementation that seems sane when assigning from a distinct object can easily fail for self-assignment. While it might seem unimaginable that client code would even attempt self-assignment, it can happen relatively easily during algo operations on containers, with x = f(x);
code where f
is (perhaps only for some #ifdef
branches) a macro ala #define f(x) x
or a function returning a reference to x
, or even (likely inefficient but concise) code like x = c1 ? x * 2 : c2 ? x / 2 : x;
). For example:
? 从不同的对象进行赋值时看似合理的赋值运算符实现很容易因自赋值而失败。虽然客户端代码甚至会尝试自赋值似乎是不可想象的,但它可以在容器上的算法操作期间相对容易地发生,x = f(x);
代码 wheref
是(可能仅针对某些#ifdef
分支)宏 ala#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_);
}
...
};
On self-assignment, the above code delete's x.p_;
, points p_
at a newly allocated heap region, then attempts to read the uninitialiseddata therein (Undefined Behaviour), if that doesn't do anything too weird, copy
attempts a self-assignment to every just-destructed 'T'!
在自赋值时,上面的代码 delete'sx.p_;
指向p_
一个新分配的堆区域,然后尝试读取其中的未初始化数据(未定义行为),如果这不会做任何太奇怪的事情,则尝试对每个刚分配copy
的自赋值 -破坏了'T'!
? The copy-and-swap idiom can introduce inefficiencies or limitations due to the use of an extra temporary (when the operator's parameter is copy-constructed):
? 由于使用了额外的临时变量(当运算符的参数是复制构造的),复制和交换习语可能会导致效率低下或限制:
struct Client
{
IP_Address ip_address_;
int socket_;
X(const X& rhs)
: ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
{ }
};
Here, a hand-written Client::operator=
might check if *this
is already connected to the same server as rhs
(perhaps sending a "reset" code if useful), whereas the copy-and-swap approach would invoke the copy-constructor which would likely be written to open a distinct socket connection then close the original one. Not only could that mean a remote network interaction instead of a simple in-process variable copy, it could run afoul of client or server limits on socket resources or connections. (Of course this class has a pretty horrid interface, but that's another matter ;-P).
在这里,手写Client::operator=
可能会检查是否*this
已经连接到相同的服务器rhs
(如果有用,可能会发送“重置”代码),而复制和交换方法将调用可能被写入打开的复制构造函数一个不同的套接字连接然后关闭原来的一个。这不仅意味着远程网络交互而不是简单的进程内变量复制,它还可能违反客户端或服务器对套接字资源或连接的限制。(当然这个类有一个非常可怕的界面,但那是另一回事;-P)。
回答by Oleksiy
This answer is more like an addition and a slight modification to the answers above.
这个答案更像是对上述答案的补充和轻微修改。
In some versions of Visual Studio (and possibly other compilers) there is a bug that is really annoying and doesn't make sense. So if you declare/define your swap
function like this:
在 Visual Studio 的某些版本(可能还有其他编译器)中,存在一个非常烦人且没有意义的错误。因此,如果您swap
像这样声明/定义您的函数:
friend void swap(A& first, A& second) {
std::swap(first.size, second.size);
std::swap(first.arr, second.arr);
}
... the compiler will yell at you when you call the swap
function:
...当您调用该swap
函数时,编译器会对您大喊大叫:
This has something to do with a friend
function being called and this
object being passed as a parameter.
这与friend
被调用的函数和this
作为参数传递的对象有关。
A way around this is to not use friend
keyword and redefine the swap
function:
解决此问题的一种方法是不使用friend
关键字并重新定义swap
函数:
void swap(A& other) {
std::swap(size, other.size);
std::swap(arr, other.arr);
}
This time, you can just call swap
and pass in other
, thus making the compiler happy:
这一次,您可以调用swap
并传入other
,从而使编译器满意:
After all, you don't needto use a friend
function to swap 2 objects. It makes just as much sense to make swap
a member function that has one other
object as a parameter.
毕竟,您不需要使用friend
函数来交换 2 个对象。制作swap
一个以一个other
对象作为参数的成员函数也同样有意义。
You already have access to this
object, so passing it in as a parameter is technically redundant.
您已经有权访问this
对象,因此将其作为参数传入在技术上是多余的。
回答by Kerrek SB
I would like to add a word of warning when you are dealing with C++11-style allocator-aware containers. Swapping and assignment have subtly different semantics.
当您处理 C++11-style allocator-aware 容器时,我想添加一个警告。交换和赋值的语义略有不同。
For concreteness, let us consider a container std::vector<T, A>
, where A
is some stateful allocator type, and we'll compare the following functions:
为了具体起见,让我们考虑一个 container 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);
}
The purpose of both functions fs
and fm
is to give a
the state that b
had initially. However, there is a hidden question: What happens if a.get_allocator() != b.get_allocator()
? The answer is: It depends. Let's write AT = std::allocator_traits<A>
.
这两种功能的目的fs
,并fm
为给a
该国b
已经开始。然而,有一个隐藏的问题:如果a.get_allocator() != b.get_allocator()
? 答案是:视情况而定。让我们写AT = std::allocator_traits<A>
。
If
AT::propagate_on_container_move_assignment
isstd::true_type
, thenfm
reassigns the allocator ofa
with the value ofb.get_allocator()
, otherwise it does not, anda
continues to use its original allocator. In that case, the data elements need to be swapped individually, since the storage ofa
andb
is not compatible.If
AT::propagate_on_container_swap
isstd::true_type
, thenfs
swaps both data and allocators in the expected fashion.If
AT::propagate_on_container_swap
isstd::false_type
, then we need a dynamic check.- If
a.get_allocator() == b.get_allocator()
, then the two containers use compatible storage, and swapping proceeds in the usual fashion. - However, if
a.get_allocator() != b.get_allocator()
, the program has undefined behaviour(cf. [container.requirements.general/8].
- If
如果
AT::propagate_on_container_move_assignment
是std::true_type
,则用 的值fm
重新分配 的分配器,否则不会,并继续使用其原始分配器。在这种情况下,数据元素需要被单独地交换,因为的存储和不兼容。a
b.get_allocator()
a
a
b
如果
AT::propagate_on_container_swap
是std::true_type
,fs
则以预期的方式交换数据和分配器。如果
AT::propagate_on_container_swap
是std::false_type
,那么我们需要动态检查。- 如果
a.get_allocator() == b.get_allocator()
,则两个容器使用兼容的存储,并以通常的方式进行交换。 - 但是,如果
a.get_allocator() != b.get_allocator()
,则程序具有未定义的行为(参见 [container.requirements.general/8]。
- 如果
The upshot is that swapping has become a non-trivial operation in C++11 as soon as your container starts supporting stateful allocators. That's a somewhat "advanced use case", but it's not entirely unlikely, since move optimizations usually only become interesting once your class manages a resource, and memory is one of the most popular resources.
结果是,一旦您的容器开始支持有状态分配器,交换就成为 C++11 中的一项重要操作。这是一个有点“高级用例”,但并非完全不可能,因为移动优化通常只有在您的类管理资源时才会变得有趣,而内存是最受欢迎的资源之一。