哇,这里要清理的东西太多了...
首先,“ 复制和交换”并非始终是实现“复制分配”的正确方法。几乎可以肯定的是dumb_array
,这是次优的解决方案。
使用复制和交换为dumb_array
是把与最大特征的最昂贵的操作在底部层的一个典型的例子。对于想要最完整功能并愿意支付性能损失的客户而言,它是完美的选择。他们得到了他们想要的东西。
但这对于不需要最完整功能的客户来说却是灾难性的。对于他们dumb_array
来说,这只是他们不得不重写的另一款软件,因为它太慢了。如果dumb_array
设计不同,它可以使两个客户满意而不会折衷任何一个客户。
满足两个客户端的关键是在最低级别上构建最快的操作,然后在API之上添加API以获得更全面的功能,而费用更高。也就是说,您需要强有力的例外保证,罚款,您为此付费。你不需要吗 这是一个更快的解决方案。
让我们具体点:这是快速,基本的例外保证Copy Assignment运算符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)
仅对移出的值进行自移动分配。在我们的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;
}
如果您自分配两个从(空)移来dumb_array
的值,则除了在程序中插入无用的指令外,您不会做任何错误的事情。可以对绝大多数对象进行相同的观察。
<
更新资料>
我对这个问题进行了更多的思考,并改变了我的立场。我现在认为分配应该容忍自我分配,但是副本分配和移动分配的职位条件是不同的:
对于副本分配:
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
可以保持互斥锁或文件的打开状态,则客户端可以合理地期望立即释放移动分配的lh上的那些资源,因此此实现可能会出现问题。
第一家的成本是两家额外的商店。第二个的成本是测试和分支。两者都可以。两者都满足C ++ 11标准中表22 MoveAssignable的所有要求。第三个也以非内存资源问题为模。
取决于硬件,这三种实现方式的成本都可能不同:分支的价格如何?登记册是否很多?
得出的结论是,与自我复制分配不同,自我移动分配不必保留当前值。
<
/更新>
受Luc Danton评论的启发,进行了最后(希望如此)的编辑:
如果您正在编写不直接管理内存的高级类(但是可能有基类或成员可以这样做),那么移动分配的最佳实现通常是:
Class& operator=(Class&&) = default;
这将依次移动分配每个基础和每个成员,并且不包括this != &other
检查。假设您的基础和成员之间无需维护不变性,这将为您提供最高的性能和基本的异常安全性。对于需要强大异常安全性的客户,请将其指向strong_assign
。