Class& Class::operator=(const Class& rhs) {
if (this != &rhs) {
// do the assignment
}
return *this;
}
你需要对移动赋值运算符也做同样的事情吗?是否存在一种情况,this == &rhs
会是真的?
? Class::operator=(Class&& rhs) {
?
}
Class& Class::operator=(const Class& rhs) {
if (this != &rhs) {
// do the assignment
}
return *this;
}
你需要对移动赋值运算符也做同样的事情吗?是否存在一种情况,this == &rhs
会是真的?
? Class::operator=(Class&& rhs) {
?
}
dumb_array
的情况下,这几乎肯定是一个次优的解决方案。dumb_array
使用复制和交换是将最昂贵的操作与最完整的功能放在底层的经典示例。它非常适合那些想要最完整功能并愿意承担性能损失的客户。他们得到了他们想要的东西。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;
}
解释:
在现代硬件上,你可以做的比较昂贵的事情之一就是访问堆。尽可能避免访问堆是值得花费时间和精力的。 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;
}
现在回到原始问题(此时存在拼写错误):
Class&
Class::operator=(Class&& rhs)
{
if (this == &rhs) // is this check needed?
{
// ...
}
return *this;
}
Class&
Class::operator=(Class&& other)
{
assert(this != &other);
// ...
return *this;
}
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& 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
的原始值是什么,它们最终都会成为大小为零的数组。这没关系。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 上的这些资源立即释放,因此该实现可能存在问题。Class& operator=(Class&&) = default;
this != &other
检查。这将为您提供最高的性能和基本的异常安全性,假设在您的基类和成员之间不需要维护任何不变量。对于要求强异常安全性的客户,请指向strong_assign
。std::swap(x,x)
上崩溃,那么我为什么要相信它能够正确地处理更复杂的操作呢? - Quuxplusonestd::swap(x, x)
,即使当x = std::move(x)
产生未指定的结果时,它仍然可以正常工作。试试吧!你不必相信我。 - Howard Hinnantx = move(x)
让 x
处于可移动状态,swap
就能正常工作。而且 std::copy
/std::move
算法已经被定义为在无操作复制时产生未定义的行为(痛苦;20 年前的 memmove
能正确处理简单情况,但 std::move
却不能!)。所以我想我还没有想到一个关于自赋值的“绝杀”方案。但显然,无论标准是否支持,自赋值在实际代码中都经常发生。 - Quuxplusoneconst
变量可能会虚假失败”(DR 253)。我正在收集这样的失败列表,以便在今年晚些时候进行演示,即使我没有与可以实际修复这些问题的任何人交流。 - Quuxplusone首先,您在移动赋值运算符的签名中出现错误。由于移动会从源对象中窃取资源,因此源对象必须是非const
右值引用。
Class &Class::operator=( Class &&rhs ) {
//...
return *this;
}
请注意,您仍然通过非const
的l-value引用返回。
对于任一类型的直接赋值操作,标准不是为了检查自我赋值,而是确保自我赋值不会导致崩溃。通常情况下,没有人明确执行 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;
}
这个复制赋值运算符可以优雅地处理自我赋值,而无需显式检查。如果源对象和目标对象的大小不同,则在复制之前进行释放和重新分配。否则,只执行复制操作。自我赋值没有获得优化路径,它被转储到与源对象和目标对象大小相等时相同的路径中。当两个对象等价(包括它们是相同对象时),复制实际上是不必要的,但这就是在不执行相等性检查(按值或地址)时的代价,因为大多数情况下该检查本身都是浪费的。请注意,此处对象的自我赋值将导致一系列元素级别的自我赋值;元素类型必须安全才能执行此操作。
与其来源示例一样,这个复制赋值运算符提供了基本的异常安全保证。如果您想要强烈的保证,请使用原始的Copy and Swap查询中的统一赋值运算符,它处理复制和移动赋值。但这个例子的重点是通过减少一个安全级别来换取速度。(顺便说一句,我们假设各个元素的值是独立的;即不存在某些值与其他值相比的不变约束。)
现在让我们看看同一类型的移动赋值:
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
失去了原来的资源,获取了other
最初持有的资源。然后,this
的旧资源在temp
结束时被销毁。
当出现自我赋值情况时,other
被清空到temp
时,this
也会被清空。然后,当temp
和this
交换时,目标对象将重新获取其资源。temp
的结束会清除一个空对象,这应该是几乎没有操作的。 this
/other
对象保留其资源。
只要移动构造和交换也是never-throw,移动赋值就应该也是never-throw的。在自我赋值期间也保持安全的成本比低级类型多了一些指令,但应该被释放调用所覆盖。
delete
之前,您需要检查是否分配了任何内存吗? - FreelanceConsultantstd::copy
会导致未定义行为。请参阅 C++14 [alg.copy]/3。 - M.M我属于那些希望具备自我分配安全运算符但不想在operator=
的实现中编写自我分配检查的阵营。事实上,我甚至不想完全实现operator=
,我希望默认行为能够“开箱即用”。最好的特殊成员是免费的。
话虽如此,标准中描述的MoveAssignable要求如下(来自17.6.3.1模板参数要求[utility.arg.requirements],n3290):
表达式 返回类型 返回值 后置条件 t = rv T& t t等价于赋值之前rv的值
其中占位符描述为:“t
[是] 可修改的T类型左值;”和“rv
是T类型的右值。”请注意,这些是放在标准库模板参数中使用的类型上的要求,但是在标准中寻找其他移动分配要求时,我注意到每个移动分配要求都类似于这个。
这意味着a = std::move(a)
必须是“安全”的。如果你需要进行身份测试(例如this != &other
),那就去做吧,否则你甚至无法将对象放入std::vector
!(除非你不使用那些需要MoveAssignable的成员/操作;但是不要在意这个。)请注意,对于前面的例子a = std::move(a)
,则确实会成立this == &other
。
a = std::move(a)
不起作用会导致一个类不能与 std::vector
一起工作吗?能举个例子吗? - Paul J. Lucasstd::vector<T>::erase
如果 T
不是 MoveAssignable 是不被允许的。(顺便提一句,在 C++14 中,某些 MoveAssignable 的要求已被放宽为 MoveInsertable。) - Luc DantonT
必须是可移动赋值的,但为什么 erase()
会依赖将元素移动到“它自己”呢? - Paul J. Lucasstruct 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
}
据我所了解,关于右值引用,不建议这样做(即,应该只返回一个临时值,而不是右值引用),但如果仍然这样做,那么你需要检查传入的右值引用是否引用了与this
指针相同的对象。
const
,那么您只能从中读取,因此唯一需要进行检查的情况是,如果您在operator =(const T &&)
中决定执行与典型的operator =(const T&)
方法相同的重新初始化操作,而不是交换式操作(即,窃取指针等而不是进行深层复制)。 - Jason我的回答仍然是移动赋值不必对自我赋值进行保存,但是它有一个不同的解释。考虑std::unique_ptr。如果我要实现一个,我会像这样做:
unique_ptr& operator=(unique_ptr&& x) {
delete ptr_;
ptr_ = x.ptr_;
x.ptr_ = nullptr;
return *this;
}
src.erase(
std::partition_copy(src.begin(), src.end(),
src.begin(),
std::back_inserter(even),
[](int num) { return num % 2; }
).first,
src.end());
对于整数来说这是可以的,但我不认为你可以使用移动语义使其正常工作。
总之:将对象本身进行移动赋值不行,你必须要注意这一点。
小更新。
swap(x, x)
应该是有效的。算法喜欢这些东西!当一个边角案例能够正常工作时,这总是很好的。(虽然我还没有看到过不能免费使用它的情况,但并不意味着不存在这种情况)。unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...}
对于自我移动赋值是安全的。我能想到一种情况,即 (this == rhs)。 对于这个语句: Myclass obj; std::move(obj) = std::move(obj)
A a; a = std::move(a);
. - Xeostd::move
是正常的。然后考虑别名问题,当你深入调用堆栈并且你有一个对T
的引用以及另一个对T
的引用时...你要在这里检查身份吗?你想找到第一个调用(或调用),在那里记录无法两次传递相同参数的文档将静态证明这两个引用不会别名吗?还是你让自我赋值正常工作? - Luc Dantonstd::sort
或std::shuffle
时,自我交换是很常见的操作。当你在交换数组中第i
个和第j
个元素时,且没有先检查i != j
条件时,这种操作就会发生。(std::swap
是通过移动赋值实现的。) - Quuxplusone