C++ 移动赋值运算符和`if (this != &rhs)`
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/9322174/
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
Move assignment operator and `if (this != &rhs)`
提问by Seth Carnegie
In the assignment operator of a class, you usually need to check if the object being assigned is the invoking object so you don't screw things up:
在类的赋值运算符中,你通常需要检查被赋值的对象是否是调用对象,这样你就不会搞砸了:
Class& Class::operator=(const Class& rhs) {
if (this != &rhs) {
// do the assignment
}
return *this;
}
Do you need the same thing for the move assignment operator? Is there ever a situation where this == &rhs
would be true?
移动赋值运算符是否需要相同的东西?有没有一种情况this == &rhs
是真的?
? Class::operator=(Class&& rhs) {
?
}
回答by Howard Hinnant
Wow, there is just so much to clean up here...
哇,这里有很多东西要清理……
First, the Copy and Swapis not always the correct way to implement Copy Assignment. Almost certainly in the case of dumb_array
, this is a sub-optimal solution.
首先,复制和交换并不总是实现复制分配的正确方法。几乎可以肯定,在 的情况下dumb_array
,这是次优解决方案。
The use of Copy and Swapis for dumb_array
is a classic example of putting the most expensive operation with the fullest features at the bottom layer. It is perfect for clients who want the fullest feature and are willing to pay the performance penalty. They get exactly what they want.
使用复制和交换为dumb_array
是把与最大特征的最昂贵的操作在底部层的一个典型的例子。它非常适合想要最完整功能并愿意支付性能损失的客户。他们得到的正是他们想要的。
But it is disastrous for clients who do not need the fullest feature and are instead looking for the highest performance. For them dumb_array
is just another piece of software they have to rewrite because it is too slow. Had dumb_array
been designed differently, it could have satisfied both clients with no compromises to either client.
但对于不需要最完整功能而是寻求最高性能的客户来说,这是灾难性的。对他们dumb_array
来说只是另一个他们必须重写的软件,因为它太慢了。如果dumb_array
设计不同,它可以让两个客户都满意,而不会对任何一个客户妥协。
The key to satisfying both clients is to build the fastest operations in at the lowest level, and then to add API on top of that for fuller features at more expense. I.e. you need the strong exception guarantee, fine, you pay for it. You don't need it? Here's a faster solution.
满足两个客户的关键是在最低级别构建最快的操作,然后在其上添加 API,以更高的成本获得更完整的功能。即您需要强大的异常保证,很好,您为此付费。你不需要吗?这是一个更快的解决方案。
Let's get concrete: Here's the fast, basic exception guarantee Copy Assignment operator for dumb_array
:
让我们具体一点:这是快速、基本的异常保证复制赋值运算符dumb_array
:
dumb_array& operator=(const dumb_array& other)
{
if (this != &other)
{
if (mSize != other.mSize)
{
delete [] mArray;
mArray = nullptr;
mArray = other.mSize ? new int[other.mSize] : nullptr;
mSize = other.mSize;
}
std::copy(other.mArray, other.mArray + mSize, mArray);
}
return *this;
}
Explanation:
解释:
One of the more expensive things you can do on modern hardware is make a trip to the heap. Anything you can do to avoid a trip to the heap is time & effort well spent. Clients of dumb_array
may well want to often assign arrays of the same size. And when they do, all you need to do is a memcpy
(hidden under std::copy
). You don't want to allocate a new array of the same size and then deallocate the old one of the same size!
您可以在现代硬件上做的更昂贵的事情之一是访问堆。您可以做的任何事情来避免访问堆,都是值得的时间和精力。的客户dumb_array
可能希望经常分配相同大小的数组。当他们这样做时,您需要做的就是memcpy
(隐藏在 下std::copy
)。您不想分配相同大小的新数组,然后释放相同大小的旧数组!
Now for your clients who actually want strong exception safety:
现在对于真正想要强大异常安全的客户:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
swap(lhs, rhs);
return lhs;
}
Or maybe if you want to take advantage of move assignment in C++11 that should be:
或者,如果您想利用 C++11 中的移动赋值,应该是:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
lhs = std::move(rhs);
return lhs;
}
If dumb_array
's clients value speed, they should call the operator=
. If they need strong exception safety, there are generic algorithms they can call that will work on a wide variety of objects and need only be implemented once.
如果dumb_array
客户重视速度,他们应该调用operator=
. 如果他们需要强大的异常安全性,他们可以调用通用算法,这些算法将适用于各种对象,并且只需要实现一次。
Now back to the original question (which has a type-o at this point in time):
现在回到最初的问题(此时有一个 o 型):
Class&
Class::operator=(Class&& rhs)
{
if (this == &rhs) // is this check needed?
{
// ...
}
return *this;
}
This is actually a controversial question. Some will say yes, absolutely, some will say no.
这实际上是一个有争议的问题。有些人会说是的,绝对的,有些人会说不。
My personal opinion is no, you don't need this check.
我个人的意见是不,你不需要这张支票。
Rationale:
理由:
When an object binds to an rvalue reference it is one of two things:
当一个对象绑定到一个右值引用时,它是两件事之一:
- A temporary.
- An object the caller wants you to believe is a temporary.
- 一个临时的。
- 调用者希望您相信的对象是临时对象。
If you have a reference to an object that is an actual temporary, then by definition, you have a unique reference to that object. It can't possibly be referenced by anywhere else in your entire program. I.e. this == &temporary
is not possible.
如果你有一个对一个实际临时对象的引用,那么根据定义,你有一个对该对象的唯一引用。它不可能被整个程序中的任何其他地方引用。即this == &temporary
不可能。
Now if your client has lied to you and promised you that you're getting a temporary when you're not, then it is the client's responsibility to be sure that you don't have to care. If you want to be really careful, I believe that this would be a better implementation:
现在,如果您的客户对您撒谎并承诺您会得到一个临时的,而您却没有,那么客户有责任确保您不必关心。如果您想非常小心,我相信这将是一个更好的实现:
Class&
Class::operator=(Class&& other)
{
assert(this != &other);
// ...
return *this;
}
I.e. If you arepassed a self reference, this is a bug on the part of the client that should be fixed.
即如果你被传递了一个自引用,这是客户端的一个错误,应该被修复。
For completeness, here is a move assignment operator for dumb_array
:
为了完整起见,这里是 的移动赋值运算符dumb_array
:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
In the typical use case of move assignment, *this
will be a moved-from object and so delete [] mArray;
should be a no-op. It is critical that implementations make delete on a nullptr as fast as possible.
在移动分配的典型用例中,*this
将是一个移动对象,因此delete [] mArray;
应该是一个空操作。实现尽可能快地在 nullptr 上进行删除是至关重要的。
Caveat:
警告:
Some will argue that swap(x, x)
is a good idea, or just a necessary evil. And this, if the swap goes to the default swap, can cause a self-move-assignment.
有些人会争辩说这swap(x, x)
是一个好主意,或者只是一个必要的邪恶。并且,如果交换进入默认交换,则可能导致自移动分配。
I disagree that swap(x, x)
is evera good idea. If found in my own code, I will consider it a performance bug and fix it. But in case you want to allow it, realize that swap(x, x)
only does self-move-assignemnet on a moved-from value. And in our dumb_array
example this will be perfectly harmless if we simply omit the assert, or constrain it to the moved-from case:
我不同意这swap(x, x)
是有史以来一个好主意。如果在我自己的代码中发现,我会认为它是一个性能错误并修复它。但是,如果您想允许它,请意识到swap(x, x)
仅对移动的值进行自我移动分配。在我们的dumb_array
示例中,如果我们简单地省略断言或将其限制为移动的情况,这将是完全无害的:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other || mSize == 0);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
If you self-assign two moved-from (empty) dumb_array
's, you don't do anything incorrect aside from inserting useless instructions into your program. This same observation can be made for the vast majority of objects.
如果您自行分配两个移动的 (空) dumb_array
',除了在程序中插入无用的指令外,您不会做任何不正确的事情。可以对绝大多数对象进行相同的观察。
<
Update>
<
更新>
I've given this issue some more thought, and changed my position somewhat. I now believe that assignment should be tolerant of self assignment, but that the post conditions on copy assignment and move assignment are different:
我对这个问题进行了更多思考,并稍微改变了我的立场。我现在认为赋值应该容忍自赋值,但是复制赋值和移动赋值的后置条件是不同的:
For copy assignment:
对于副本分配:
x = y;
one should have a post-condition that the value of y
should not be altered. When &x == &y
then this postcondition translates into: self copy assignment should have no impact on the value of x
.
应该有一个后置条件,即y
不应更改的值。当&x == &y
那么这个后置条件转换为:自我拷贝赋值应该对价值没有影响x
。
For move assignment:
对于移动分配:
x = std::move(y);
one should have a post-condition that y
has a valid but unspecified state. When &x == &y
then this postcondition translates into: x
has a valid but unspecified state. I.e. self move assignment does not have to be a no-op. But it should not crash. This post-condition is consistent with allowing swap(x, x)
to just work:
应该有一个y
具有有效但未指定状态的后置条件。当&x == &y
此后置条件转换为:x
具有有效但未指定的状态时。即自移动分配不必是空操作。但它不应该崩溃。这个后置条件与允许swap(x, x)
只工作是一致的:
template <class T>
void
swap(T& x, T& y)
{
// assume &x == &y
T tmp(std::move(x));
// x and y now have a valid but unspecified state
x = std::move(y);
// x and y still have a valid but unspecified state
y = std::move(tmp);
// x and y have the value of tmp, which is the value they had on entry
}
The above works, as long as x = std::move(x)
doesn't crash. It can leave x
in any valid but unspecified state.
以上工作,只要x = std::move(x)
不崩溃。它可以x
处于任何有效但未指定的状态。
I see three ways to program the move assignment operator for dumb_array
to achieve this:
我看到了三种对移动赋值运算符进行编程dumb_array
以实现此目的的方法:
dumb_array& operator=(dumb_array&& other)
{
delete [] mArray;
// set *this to a valid state before continuing
mSize = 0;
mArray = nullptr;
// *this is now in a valid state, continue with move assignment
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
The above implementation tolerates self assignment, but *this
and other
end up being a zero-sized array after the self-move assignment, no matter what the original value of *this
is. This is fine.
上述实施容忍自我分配,但是*this
并other
最终成为自招分配后一个零大小的数组,不管是什么的原值*this
为。这可以。
dumb_array& operator=(dumb_array&& other)
{
if (this != &other)
{
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
}
return *this;
}
The above implementation tolerates self assignment the same way the copy assignment operator does, by making it a no-op. This is also fine.
上述实现通过使其成为无操作来容忍自赋值,就像复制赋值运算符所做的那样。这也很好。
dumb_array& operator=(dumb_array&& other)
{
swap(other);
return *this;
}
The above is ok only if dumb_array
does not hold resources that should be destructed "immediately". For example if the only resource is memory, the above is fine. If dumb_array
could possibly hold mutex locks or the open state of files, the client could reasonably expect those resources on the lhs of the move assignment to be immediately released and therefore this implementation could be problematic.
仅当dumb_array
不持有应“立即”销毁的资源时,以上才可以。例如,如果唯一的资源是内存,上面的就可以了。如果dumb_array
可能持有互斥锁或文件的打开状态,客户端可以合理地期望移动分配的 lhs 上的那些资源立即释放,因此此实现可能有问题。
The cost of the first is two extra stores. The cost of the second is a test-and-branch. Both work. Both meet all of the requirements of Table 22 MoveAssignable requirements in the C++11 standard. The third also works modulo the non-memory-resource-concern.
第一个的成本是两个额外的商店。第二个的成本是测试和分支。两者都有效。两者都满足 C++11 标准中表 22 MoveAssignable 要求的所有要求。第三个也以非内存资源关注为模。
All three implementations can have different costs depending on the hardware: How expensive is a branch? Are there lots of registers or very few?
根据硬件的不同,所有三种实现都有不同的成本:分支有多贵?寄存器是多还是少?
The take-away is that self-move-assignment, unlike self-copy-assignment, does not have to preserve the current value.
结论是,自移动赋值与自复制赋值不同,不必保留当前值。
<
/Update>
<
/更新>
One final (hopefully) edit inspired by Luc Danton's comment:
受 Luc Danton 评论启发的最后一次(希望如此)编辑:
If you're writing a high level class that doesn't directly manage memory (but may have bases or members that do), then the best implementation of move assignment is often:
如果你正在编写一个不直接管理内存的高级类(但可能有基础或成员),那么移动分配的最佳实现通常是:
Class& operator=(Class&&) = default;
This will move assign each base and each member in turn, and will not include a this != &other
check. This will give you the very highest performance and basic exception safety assuming no invariants need to be maintained among your bases and members. For your clients demanding strong exception safety, point them towards strong_assign
.
这将依次分配每个基地和每个成员,并且不包括this != &other
检查。这将为您提供最高的性能和基本的异常安全性,假设您的基础和成员之间不需要保持不变。对于需要强大异常安全性的客户,请将他们指向strong_assign
.
回答by CTMacUser
First, you got the signature of the move-assignment operator wrong. Since moves steal resources from the source object, the source has to be a non-const
r-value reference.
首先,您弄错了移动赋值运算符的签名。由于移动会从源对象中窃取资源,因此源必须是非const
右值引用。
Class &Class::operator=( Class &&rhs ) {
//...
return *this;
}
Note that you still return via a (non-const
) l-value reference.
请注意,您仍然通过(非const
)l值引用返回。
For either type of direct assignment, the standard isn't to check for self-assignment, but to make sure a self-assignment doesn't cause a crash-and-burn. Generally, no one explicitly does x = x
or y = std::move(y)
calls, but aliasing, especially through multiple functions, may lead a = b
or c = std::move(d)
into being self-assignments. An explicit check for self-assignment, i.e. this == &rhs
, that skips the meat of the function when true is one way to ensure self-assignment safety. But it's one of the worst ways, since it optimizes a (hopefully) rare case while it's an anti-optimization for the more common case (due to branching and possibly cache misses).
对于任一类型的直接分配,标准不是检查自分配,而是确保自分配不会导致崩溃和烧毁。通常,没有人明确地执行x = x
或y = std::move(y)
调用,但别名,尤其是通过多个函数,可能导致a = b
或c = std::move(d)
成为自赋值。对自赋值的显式检查,即this == &rhs
,在 true 时跳过函数的内容是确保自赋值安全的一种方法。但这是最糟糕的方法之一,因为它优化了(希望如此)罕见的情况,而它是更常见情况的反优化(由于分支和可能的缓存未命中)。
Now when (at least) one of the operands is a directly temporary object, you can never have a self-assignment scenario. Some people advocate assuming that case and optimize the code for it so much that the code becomes suicidally stupid when the assumption is wrong. I say that dumping the same-object check on users is irresponsible. We don't make that argument for copy-assignment; why reverse the position for move-assignment?
现在,当(至少)其中一个操作数是直接临时对象时,您永远不会有自赋值场景。有些人主张假设这种情况并为其优化代码,以至于当假设错误时,代码变得愚蠢至极。我说对用户倾销同对象检查是不负责任的。我们不会为复制赋值提出这个论点;为什么要反转移动分配的位置?
Let's make an example, altered from another respondent:
让我们举一个例子,从另一个受访者那里改过来的:
dumb_array& dumb_array::operator=(const dumb_array& other)
{
if (mSize != other.mSize)
{
delete [] mArray;
mArray = nullptr; // clear this...
mSize = 0u; // ...and this in case the next line throws
mArray = other.mSize ? new int[other.mSize] : nullptr;
mSize = other.mSize;
}
std::copy(other.mArray, other.mArray + mSize, mArray);
return *this;
}
This copy-assignment handles self-assignment gracefully without an explicit check. If the source and destination sizes differ, then deallocation and reallocation precede the copying. Otherwise, just the copying is done. Self-assignment does not get an optimized path, it gets dumped into the same path as when the source and destination sizes start off equal. The copying is technically unnecessary when the two objects are equivalent (including when they're the same object), but that's the price when not doing an equality check (value-wise or address-wise) since said check itself would be a waste most of the time. Note that the object self-assignment here will cause a series of element-level self-assignments; the element type has to be safe for doing this.
这种复制赋值可以优雅地处理自赋值,而无需显式检查。如果源和目标大小不同,则在复制之前释放和重新分配。否则,只是复制完成。自分配不会获得优化的路径,它会被转储到与源和目标大小开始相等时相同的路径中。当两个对象相等时(包括当它们是同一个对象时),复制在技术上是不必要的,但这是不进行相等检查(值或地址)时的代价,因为所述检查本身将是最浪费的的时间。注意这里的对象自赋值会引起一系列元素级的自赋值;元素类型必须是安全的才能做到这一点。
Like its source example, this copy-assignment provides the basic exception safety guarantee. If you want the strong guarantee, then use the unified-assignment operator from the original Copy and Swapquery, which handles both copy- and move-assignment. But the point of this example is to reduce safety by one rank to gain speed. (BTW, we're assuming that the individual elements' values are independent; that there's no invariant constraint limiting some values compared to others.)
与其源示例一样,此复制赋值提供了基本的异常安全保证。如果您想要强有力的保证,那么使用原始Copy and Swap查询中的统一赋值运算符,它处理复制和移动赋值。但是这个例子的重点是将安全性降低一级以提高速度。(顺便说一句,我们假设单个元素的值是独立的;与其他值相比,没有限制某些值的不变约束。)
Let's look at a move-assignment for this same type:
让我们看一下相同类型的移动赋值:
class dumb_array
{
//...
void swap(dumb_array& other) noexcept
{
// Just in case we add UDT members later
using std::swap;
// both members are built-in types -> never throw
swap( this->mArray, other.mArray );
swap( this->mSize, other.mSize );
}
dumb_array& operator=(dumb_array&& other) noexcept
{
this->swap( other );
return *this;
}
//...
};
void swap( dumb_array &l, dumb_array &r ) noexcept { l.swap( r ); }
A swappable type that needs customization should have a two-argument free function called swap
in the same namespace as the type. (The namespace restriction lets unqualified calls to swap to work.) A container type should also add a public swap
member function to match the standard containers. If a member swap
is not provided, then the free-function swap
probably needs to be marked as a friend of the swappable type. If you customize moves to use swap
, then you have to provide your own swapping code; the standard code calls the type's move code, which would result in infinite mutual recursion for move-customized types.
需要自定义的可交换类型应该具有swap
在与类型相同的命名空间中调用的无两个参数的函数。(命名空间限制允许对 swap 的非限定调用起作用。)容器类型还应该添加一个公共swap
成员函数以匹配标准容器。如果swap
未提供成员,则swap
可能需要将自由函数标记为可交换类型的友元。如果你自定义动作来使用swap
,那么你必须提供你自己的交换代码;标准代码调用类型的移动代码,这将导致移动自定义类型的无限相互递归。
Like destructors, swap functions and move operations should be never-throw if at all possible, and probably marked as such (in C++11). Standard library types and routines have optimizations for non-throwable moving types.
像析构函数一样,如果可能,交换函数和移动操作应该永远不会抛出,并且可能标记为这样(在 C++11 中)。标准库类型和例程对不可抛出的移动类型进行了优化。
This first version of move-assignment fulfills the basic contract. The source's resource markers are transferred to the destination object. The old resources won't be leaked since the source object now manages them. And the source object is left in a usable state where further operations, including assignment and destruction, can be applied to it.
移动分配的第一个版本满足了基本契约。源的资源标记被转移到目标对象。旧资源不会泄漏,因为源对象现在管理它们。并且源对象处于可用状态,可以对其应用进一步的操作,包括赋值和销毁。
Note that this move-assignment is automatically safe for self-assignment, since the swap
call is. It's also strongly exception safe. The problem is unnecessary resource retention. The old resources for the destination are conceptually no longer needed, but here they are still around only so the source object can stay valid. If the scheduled destruction of the source object is a long way off, we're wasting resource space, or worse if the total resource space is limited and other resource petitions will happen before the (new) source object officially dies.
请注意,此移动分配对于自分配是自动安全的,因为swap
调用是安全的。它也是强烈异常安全的。问题是不必要的资源保留。目标的旧资源在概念上不再需要,但在这里它们仍然存在,只是为了源对象可以保持有效。如果源对象的预定销毁还有很长的路要走,我们就是在浪费资源空间,或者更糟的是,如果总资源空间有限,并且在(新)源对象正式消亡之前会发生其他资源请求。
This issue is what caused the controversial current guru advice concerning self-targeting during move-assignment. The way to write move-assignment without lingering resources is something like:
这个问题是导致当前关于在移动分配期间自我定位的有争议的专家建议的原因。在没有挥之不去的资源的情况下编写移动分配的方法是这样的:
class dumb_array
{
//...
dumb_array& operator=(dumb_array&& other) noexcept
{
delete [] this->mArray; // kill old resources
this->mArray = other.mArray;
this->mSize = other.mSize;
other.mArray = nullptr; // reset source
other.mSize = 0u;
return *this;
}
//...
};
The source is reset to default conditions, while the old destination resources are destroyed. In the self-assignment case, your current object ends up committing suicide. The main way around it is to surround the action code with an if(this != &other)
block, or screw it and let clients eat an assert(this != &other)
initial line (if you're feeling nice).
源被重置为默认条件,而旧的目标资源被销毁。在自我分配的情况下,您当前的对象最终会自杀。解决它的主要方法是用一个if(this != &other)
块包围动作代码,或者把它搞砸并让客户吃一个assert(this != &other)
初始行(如果你感觉不错的话)。
An alternative is to study how to make copy-assignment strongly exception safe, without unified-assignment, and apply it to move-assignment:
另一种方法是研究如何在没有统一分配的情况下使复制分配强异常安全,并将其应用于移动分配:
class dumb_array
{
//...
dumb_array& operator=(dumb_array&& other) noexcept
{
dumb_array temp{ std::move(other) };
this->swap( temp );
return *this;
}
//...
};
When other
and this
are distinct, other
is emptied by the move to temp
and stays that way. Then this
loses its old resources to temp
while getting the resources originally held by other
. Then the old resources of this
get killed when temp
does.
当other
和this
是不同的时,other
通过移动到temp
并保持那样被清空。然后在得到原来持有的资源的同时this
失去它的旧资源。那么老资源什么时候弄死的呢。temp
other
this
temp
When self-assignment happens, the emptying of other
to temp
empties this
as well. Then target object gets its resources back when temp
and this
swap. The death of temp
claims an empty object, which should be practically a no-op. The this
/other
object keeps its resources.
当自赋值发生时,清空other
to 也temp
清空this
。然后,目标对象获得资源回来时temp
和this
交换。死亡temp
声称一个空对象,这实际上应该是一个空操作。该this
/other
对象保持其资源。
The move-assignment should be never-throw as long as move-construction and swapping are also. The cost of also being safe during self-assignment is a few more instructions over low-level types, which should be swamped out by the deallocation call.
只要移动构造和交换也是如此,移动分配就应该永远不会抛出。在自分配期间也是安全的成本是在低级别类型上的更多指令,这些指令应该被释放调用淹没。
回答by Luc Danton
I'm in the camp of those that want self-assignment safe operators, but don't want to write self-assignment checks in the implementations of operator=
. And in fact I don't even want to implement operator=
at all, I want the default behaviour to work 'right out of the box'. The best special members are those that come for free.
我站在那些想要自赋值安全运算符的阵营,但不想在operator=
. 事实上,我什至根本不想实现operator=
,我希望默认行为“开箱即用”。最好的特别会员是那些免费来的会员。
That being said, the MoveAssignable requirements present in the Standard are described as follows (from 17.6.3.1 Template argument requirements [utility.arg.requirements], n3290):
也就是说,标准中存在的 MoveAssignable 要求描述如下(来自 17.6.3.1 模板参数要求 [utility.arg.requirements],n3290):
Expression Return type Return value Post-condition t = rv T& t t is equivalent to the value of rv before the assignment
where the placeholders are described as: "t
[is a] modifiable lvalue of type T;" and "rv
is an rvalue of type T;". Note that those are requirements put on the types used as arguments to the templates of the Standard library, but looking elsewhere in the Standard I notice that every requirement on move assignment is similar to this one.
其中占位符被描述为:“ t
[是] T 类型的可修改左值;” 并且“rv
是T类型的右值;”。请注意,这些是对用作标准库模板参数的类型的要求,但查看标准的其他地方,我注意到移动分配的每个要求都与此相似。
This means that a = std::move(a)
has to be 'safe'. If what you need is an identity test (e.g. this != &other
), then go for it, or else you won't even be able to put your objects into std::vector
! (Unless you don't use those members/operations that do require MoveAssignable; but nevermind that.) Notice that with the previous example a = std::move(a)
, then this == &other
will indeed hold.
这意味着a = std::move(a)
必须是“安全的”。如果您需要的是身份测试(例如this != &other
),那就去做吧,否则您甚至无法将对象放入std::vector
! (除非您不使用那些确实需要 MoveAssignable 的成员/操作;但不要紧。)请注意,在前面的示例中a = std::move(a)
, thenthis == &other
确实会成立。
回答by Jason
As your current operator=
function is written, since you've made the rvalue-reference argument const
, there is no way you could "steal" the pointers and change the values of the incoming rvalue reference... you simply can't change it, you could only read from it. I would only see an issue if you were to start calling delete
on pointers, etc. in your this
object like you would in a normal lvaue-reference operator=
method, but that sort of defeats the point of the rvalue-version ... i.e., it would seem redundant to use the rvalue version to basically do the same operations normally left to a const
-lvalue operator=
method.
在operator=
编写当前函数时,由于您已经创建了 rvalue-reference 参数const
,因此您无法“窃取”指针并更改传入的右值引用的值……您根本无法更改它,您只能从中读取。我只会看到一个问题,如果你开始delete
在你的this
对象中调用指针等,就像你在普通的 lvaue 引用operator=
方法中一样,但这种方式会破坏右值版本的要点......即,它会使用 rvalue 版本基本上执行通常留给const
-lvalueoperator=
方法的相同操作似乎是多余的。
Now if you defined your operator=
to take a non-const
rvalue-reference, then the only way I could see a check being required was if you passed the this
object to a function that intentionally returned a rvalue reference rather than a temporary.
现在,如果您将您定义operator=
为采用非const
右值引用,那么我认为需要检查的唯一方法是将this
对象传递给有意返回右值引用而不是临时引用的函数。
For instance, suppose someone tried to write an operator+
function, and utilize a mix of rvalue references and lvalue references in order to "prevent" extra temporaries from being created during some stacked addition operation on the object-type:
例如,假设有人试图编写一个operator+
函数,并混合使用右值引用和左值引用,以“防止”在对象类型的某些堆叠加法操作期间创建额外的临时对象:
struct A; //defines operator=(A&& rhs) where it will "steal" the pointers
//of rhs and set the original pointers of rhs to NULL
A&& operator+(A& rhs, A&& lhs)
{
//...code
return std::move(rhs);
}
A&& operator+(A&& rhs, A&&lhs)
{
//...code
return std::move(rhs);
}
int main()
{
A a;
a = (a + A()) + A(); //calls operator=(A&&) with reference bound to a
//...rest of code
}
Now, from what I understand about rvalue references, doing the above is discouraged (i.e,. you should just return a temporary, not rvalue reference), but, if someone were to still do that, then you'd want to check to make sure the incoming rvalue-reference was not referencing the same object as the this
pointer.
现在,根据我对右值引用的理解,不鼓励执行上述操作(即,您应该只返回一个临时的,而不是右值引用),但是,如果有人仍然这样做,那么您需要检查一下确保传入的右值引用没有引用与this
指针相同的对象。
回答by Denis Yaroshevskiy
My answer is still that move assignment doesn't have to be save against self assigment, but it has a different explanation. Consider std::unique_ptr. If I were to implement one, I would do something like this:
我的答案仍然是移动分配不一定要避免自我分配,但它有不同的解释。考虑 std::unique_ptr。如果我要实现一个,我会做这样的事情:
unique_ptr& operator=(unique_ptr&& x) {
delete ptr_;
ptr_ = x.ptr_;
x.ptr_ = nullptr;
return *this;
}
If you look at Scott Meyers explaining thishe does something similar. (If you wander why not to do swap - it has one extra write). And this is not safe for self assignment.
如果你看看Scott Meyers 的解释,他做了类似的事情。(如果你徘徊为什么不进行交换 - 它有一个额外的写入)。这对于自我分配是不安全的。
Sometimes this is unfortunate. Consider moving out of the vector all even numbers:
有时这是不幸的。考虑将所有偶数移出向量:
src.erase(
std::partition_copy(src.begin(), src.end(),
src.begin(),
std::back_inserter(even),
[](int num) { return num % 2; }
).first,
src.end());
This is ok for integers but I don't believe you can make something like this work with move semantics.
这对于整数来说没问题,但我不相信你可以用移动语义来做这样的事情。
To conclude: move assignment to the object itself is not ok and you have to watch out for it.
总结:对对象本身的移动分配是不行的,你必须小心它。
Small update.
小更新。
- I disagree with Howard, which is a bad idea, but still - I think self move
assignment of "moved out" objects should work, because
swap(x, x)
should work. Algorithms love these things! It's always nice when a corner case just works. (And I am yet to see a case where it's not for free. Doesn't mean it doesn't exist though). - This is how assigning unique_ptrs is implemented in libc++:
unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...}
It's safe for self move assignment. - Core Guidelinesthink it should be OK to self move assign.
- 我不同意霍华德,这是一个坏主意,但仍然 - 我认为“移出”对象的自我移动分配应该有效,因为
swap(x, x)
应该有效。算法喜欢这些东西!当角落案例正常工作时总是很好。(而且我还没有看到它不是免费的。但这并不意味着它不存在)。 - 这就是在 libc++ 中分配 unique_ptrs 的方式:
unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...}
自移动分配是安全的。 - 核心指南认为自行移动分配应该可以。
回答by little_monster
There is a situation that (this == rhs) I can think of. For this statement: Myclass obj; std::move(obj) = std::move(obj)
有一种情况 (this == rhs) 我能想到。对于这个语句:Myclass obj; std::move(obj) = std::move(obj)