移动赋值运算符和`if(this!=&rhs)`
在一个类的赋值操作符中,通常需要检查被赋值的对象是否是调用对象,这样就不会搞砸了:
Class& Class::operator=(const Class& rhs) {
if (this != &rhs) {
// do the assignment
}
return *this;
}
移动赋值操作符需要相同的东西吗? 有没有this == &rhs
会是真的吗?
? Class::operator=(Class&& rhs) {
?
}
哇,这里要清理这么多...
首先,复制和交换并不总是实现复制分配的正确方法。 几乎可以肯定,在dumb_array
的情况下,这是一个次优的解决方案。
Copy和Swap的使用是为了dumb_array
是一个典型的例子, dumb_array
最昂贵的操作放在最底层的最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
的移动赋值运算符:
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)
只能从移动的值上自动移动assignemnet。 在我们的dumb_array
例子中,如果我们简单地忽略assert,或者将它限制在移动的情况下,这将是完全无害的:
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
。 例如,如果唯一的资源是内存,上述情况很好。 如果dumb_array
可能持有互斥锁或文件的打开状态,则客户端可以合理地期望移动分配的lhs上的资源立即释放,因此此实现可能存在问题。
第一个的成本是两个额外的商店。 第二个成本是测试和分支。 两者都有效。 两者都满足C ++ 11标准中表22 MoveAssignable要求的所有要求。 第三种模式也是模仿非内存资源关注。
所有这三种实现都可以根据硬件具有不同的成本:分支有多昂贵? 有很多寄存器还是很少?
外卖是自动分配,不同于自我分配,不必保留当前值。
<
/ Update >
最后一个(希望)编辑灵感来自Luc Danton的评论:
如果你正在编写一个不直接管理内存的高级类(但可能有这样的基础或成员),那么移动赋值的最佳实现通常是:
Class& operator=(Class&&) = default;
这将移动分配每个基地和每个成员,并且不会包含this != &other
检查。 假设在您的基地和成员之间不需要维护不变量,这将给您最高的性能和基本的异常安全性。 对于要求强大的例外安全的客户,请将其指向strong_assign
。
首先,你的移动赋值操作符的签名是错误的。 由于移动从源对象窃取资源,源必须是一个非const
r值参考。
Class &Class::operator=( Class &&rhs ) {
//...
return *this;
}
请注意,您仍然通过(非const
) l值引用返回。
对于任何类型的直接分配,标准不是检查自我分配,而是确保自我分配不会导致崩溃和烧伤。 通常,没有人明确地做x = x
或者y = std::move(y)
调用,但是混叠,特别是通过多个函数,可能导致a = b
或者c = std::move(d)
成为自赋值。 明确检查自我赋值,即this == &rhs
,在真实时跳过该函数的肉是确保自分配安全性的一种方法。 但它是最糟糕的方式之一,因为它优化了(有希望)罕见的情况,而对于更常见的情况(由于分支和可能的缓存未命中),它是一种反优化。
现在,当(至少)其中一个操作数是直接临时对象时,您永远不会拥有自我分配场景。 有些人主张假设这种情况并优化代码,以至于当假设错误时代码变得自杀愚蠢。 我说,倾销对用户的同一对象检查是不负责任的。 我们不会为了复制分配而提出这个论点; 为什么要扭转移动分配的立场?
让我们举一个例子,从另一个应答者变更:
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;
}
这种复制分配处理自我分配,没有明确的检查。 如果源和目标大小不同,则在复制之前取消分配和重新分配。 否则,只需复制完成。 自我分配没有得到优化的路径,当源和目标的大小开始相等时,它被转储到相同的路径。 当两个对象是等价的时(包括它们是同一个对象时),技术上复制是不必要的,但这是不进行相等性检查时的价格(数值方式或地址方式),因为所述检查本身将是最浪费的的时间。 请注意,此处的对象自分配将导致一系列元素级自分配; 元素类型必须是安全的。
与其源代码示例一样,此复制分配提供了基本的例外安全保证。 如果您想获得强有力的保证,则可以使用原始复制和交换查询中的统一赋值运算符,该查询处理复制和移动赋值。 但这个例子的重点在于降低一个等级的安全性以获得速度。 (顺便说一句,我们假设单个元素的值是独立的;没有不变约束限制某些值与其他值相比)。
让我们来看看这个相同类型的移动分配:
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 ); }
需要定制的可交换类型应该在类型相同的名称空间中有一个名为swap
的双参数自由函数。 (名称空间限制允许非限定调用交换来工作。)容器类型还应该添加一个公共swap
成员函数以匹配标准容器。 如果未提供成员swap
,则可能需要将自由函数swap
标记为可交换类型的朋友。 如果您自定义移动以使用swap
,那么您必须提供您自己的交换代码; 标准代码会调用类型的移动代码,这将导致移动自定义类型的无限相互递归。
像析构函数一样,交换函数和移动操作应该永远不会抛出,如果可能的话,可能标记为(在C ++ 11中)。 标准库类型和例程对不可移动的移动类型进行了优化。
这个移动作业的第一个版本符合基本合同。 源的资源标记被转移到目标对象。 源对象现在管理它们之后,旧资源不会泄露。 并且源对象保持可用状态,其中可以对其应用更多的操作,包括赋值和销毁。
请注意,由于swap
呼叫是自动分配,此移动分配自动安全。 它也是非常强大的例外安全。 问题是不必要的资源保留。 在目的地的旧资源在概念上不再需要,但在这里它们仍然只有这样才能使源对象保持有效。 如果对源对象的计划销毁还有很长的路要走,我们浪费资源空间,或者如果总资源空间有限并且其他资源申请将在(新)源对象正式死亡之前发生,则会更糟。
这个问题是什么导致了有争议的当前大师关于在移动分配期间自我定位的建议。 没有挥之不去的资源编写移动分配的方式如下所示:
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;
}
//...
};
源被重置为默认条件,而旧的目标资源被销毁。 在自我分配的情况下,你目前的对象最终会自杀。 围绕它的主要方法是用if(this != &other)
块包围动作代码,或者拧紧它,让客户吃一个assert(this != &other)
最初的行(如果你感觉很好)。
另一种方法是研究如何在不进行统一分配的情况下进行强制异常安全的复制分配,并将其应用于移动分配:
class dumb_array
{
//...
dumb_array& operator=(dumb_array&& other) noexcept
{
dumb_array temp{ std::move(other) };
this->swap( temp );
return *this;
}
//...
};
当other
人和this
是不同的, other
是通过迁移到temp
空,并保持这种方式。 然后, this
失去了旧的资源temp
在获取最初持有的资源other
。 然后,当temp
事件发生时, this
旧资源就会死亡。
当自我分配发生时,清空other
的temp
清空this
一点。 然后,目标对象获得资源回来时, temp
和this
交换。 temp
的死亡声称是一个空洞的对象,实际上这应该是一个空操作。 this
/ other
对象保留其资源。
只要移动建设和交换,移动分配应该永远不会丢失。 在自我分配期间也是安全的成本是对低级类型的更多指令,应该通过释放调用来消除。
我在那些希望自我分配安全的操作员的阵营中,但不希望在operator=
的实现中编写自我分配检查。 事实上,我甚至不想实现operator=
,我希望默认行为“开箱即用”。 最好的特殊成员是那些免费来的。
也就是说,标准中的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
其中占位符被描述为:“ t
[是a] T型可修改的左值;” 和“ rv
是类型T的右值;”。 请注意,这些是对用作标准库模板参数的类型的要求,但在标准的其他地方查看时,我注意到移动分配的每个要求与此类似。
这意味着a = std::move(a)
必须是“安全的”。 如果你需要的是身份测试(例如this != &other
),那就去做吧,否则你甚至不能把你的对象放到std::vector
! (除非你不使用那些需要MoveAssignable的成员/操作;但是不要那么强调)。注意,在前面的例子中, a = std::move(a)
,那么this == &other
将确实成立。