哇,这里有这么多要清理的东西......
首先,复制和交换并不总是实现复制分配的正确方法。几乎可以肯定,在 的情况下dumb_array
,这是一个次优解决方案。
Copy and Swap的使用dumb_array
是一个典型的例子,将最昂贵的操作与最完整的功能放在底层。它非常适合想要最完整功能并愿意支付性能损失的客户。他们得到了他们想要的。
但对于不需要最完整功能而是寻求最高性能的客户来说,这是灾难性的。对他们dumb_array
来说,这只是另一个他们必须重写的软件,因为它太慢了。如果dumb_array
设计不同,它可以满足两个客户,而不会对任何一个客户做出妥协。
满足这两个客户的关键是在最低级别构建最快的操作,然后在其之上添加 API 以获取更完整的功能,但成本更高。即,您需要强大的例外保证,很好,您需要为此付费。你不需要吗?这是一个更快的解决方案。
让我们具体一点:这是快速、基本的异常保证复制赋值运算符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;
}
解释:
您可以在现代硬件上做的更昂贵的事情之一就是去堆。你可以做的任何事情来避免去堆是花费时间和精力。的客户dumb_array
可能希望经常分配相同大小的数组。当他们这样做时,您需要做的就是一个memcpy
(隐藏在 下std::copy
)。您不想分配一个相同大小的新数组,然后释放相同大小的旧数组!
现在,对于真正需要强大异常安全性的客户:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
swap(lhs, rhs);
return lhs;
}
或者,如果您想利用 C++11 中的移动赋值,则应该是:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
lhs = std::move(rhs);
return lhs;
}
如果dumb_array
的客户重视速度,他们应该致电operator=
. 如果他们需要强大的异常安全性,他们可以调用通用算法,这些算法将适用于各种各样的对象,并且只需要实现一次。
现在回到最初的问题(此时有一个类型-o):
Class&
Class::operator=(Class&& rhs)
{
if (this == &rhs) // is this check needed?
{
// ...
}
return *this;
}
这实际上是一个有争议的问题。有些人会说是的,绝对的,有些人会说不。
我个人的意见是不,你不需要这个检查。
理由:
当一个对象绑定到一个右值引用时,它是以下两种情况之一:
- 一个临时的。
- 调用者希望您相信的对象是暂时的。
如果您对一个实际临时对象的引用,那么根据定义,您对该对象具有唯一的引用。它不可能被整个程序中的其他任何地方引用。即this == &temporary
不可能。
现在,如果您的客户对您撒谎并承诺您会得到一个临时的,而实际上您并没有,那么客户有责任确保您不必在意。如果你想非常小心,我相信这将是一个更好的实现:
Class&
Class::operator=(Class&& other)
{
assert(this != &other);
// ...
return *this;
}
即,如果您被传递一个自我引用,这是客户端的一个应该修复的错误。
为了完整起见,这里是一个移动赋值运算符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;
}
在移动赋值的典型用例中,*this
它将是一个移动对象,因此delete [] mArray;
应该是一个空操作。实现尽可能快地在 nullptr 上进行删除至关重要。
警告:
有些人会争辩说这swap(x, x)
是一个好主意,或者只是一个必要的邪恶。如果交换进入默认交换,这可能会导致自移动分配。
我不同意这swap(x, x)
是一个好主意。如果在我自己的代码中发现它,我会认为它是一个性能错误并修复它。但是,如果您想允许它,请意识到swap(x, x)
self-move-assignemnet 仅对已移动的值进行。在我们的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;
}
如果您自行分配两个 move-from (empty) dumb_array
,除了在程序中插入无用的指令之外,您不会做任何不正确的事情。对于绝大多数物体都可以进行同样的观察。
<
更新>
我对这个问题有了更多的思考,并稍微改变了我的立场。我现在认为assignment应该可以容忍self assignment,但是copy assignment和move assignment的post条件不同:
对于复制分配:
x = y;
应该有一个后置条件,即y
不应更改 的值。当&x == &y
这个后置条件转换为:自我复制分配应该对 的值没有影响x
。
对于移动分配:
x = std::move(y);
应该有一个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
}
只要x = std::move(x)
不崩溃,上述工作即可。它可以x
处于任何有效但未指定的状态。
我看到了三种方法来编程移动赋值运算符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;
}
上面的实现允许自赋值,但是在自移动赋值*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;
}
上述实现与复制赋值运算符一样,通过使其成为无操作来容忍自赋值。这也很好。
dumb_array& operator=(dumb_array&& other)
{
swap(other);
return *this;
}
仅当dumb_array
不包含应“立即”销毁的资源时,上述内容才可以。例如,如果唯一的资源是内存,上面的就可以了。如果dumb_array
可能持有互斥锁或文件的打开状态,则客户端可以合理地期望移动分配的 lhs 上的那些资源会立即释放,因此这种实现可能会出现问题。
第一个的成本是两个额外的商店。第二个成本是测试和分支。两者都有效。两者都满足 C++11 标准中表 22 MoveAssignable 要求的所有要求。第三个也适用于非内存资源问题。
根据硬件的不同,这三种实现可能有不同的成本:一个分支有多贵?有很多寄存器还是很少?
要点是,与自复制分配不同,自移动分配不必保留当前值。
<
/更新>
受 Luc Danton 评论启发的最后一次(希望如此)编辑:
如果您正在编写一个不直接管理内存的高级类(但可能有这样做的基类或成员),那么移动分配的最佳实现通常是:
Class& operator=(Class&&) = default;
这将依次分配每个基地和每个成员,并且不包括this != &other
检查。假设您的基础和成员之间不需要维护不变量,这将为您提供最高的性能和基本的异常安全性。对于要求强大的异常安全性的客户,请将他们指向strong_assign
.