移动赋值运算符和`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的情况下,这是一个次优的解决scheme。

复制和交换的使用是为了dumb_array是一个经典的例子,把最昂贵的操作与最底层的最dumb_array的function。 对于希望得到最充分的特性并愿意支付性能损失的客户来说,这是完美的。 他们得到他们想要的。

但对于不需要最大function的客户来说,这是灾难性的,而是寻求最高的性能。 对他们来说, dumb_array只是另一个必须重写的软件,因为它太慢了。 如果dumb_arraydevise不同,它可以满足两个客户,不妥协任何客户。

满足这两个客户的关键是在最低层次上构build最快的操作,然后在其上添加API,以获得更丰富的function,成本更高。 也就是说,你需要强有力的例外保证,好的,你付出代价。 你不需要它? 这是一个更快的解决scheme。

让我们来具体的:这是dumb_array的快速,基本的exception保证Copy Assignment操作符:

 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 )。 你不想分配一个相同大小的新数组,然后释放相同大小的旧数组!

现在,对于那些真正想要强大的exception安全的客户,

 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= 。 如果他们需要强大的exception安全性,那么他们可以调用的通用algorithm可以处理各种各样的对象,只需要执行一次。

现在回到原来的问题(在这个时候有一个typeso):

 Class& Class::operator=(Class&& rhs) { if (this == &rhs) // is this check needed? { // ... } return *this; } 

这实际上是一个有争议的问题。 有些人会说是的,绝对有些人会说不。

我个人的意见不是,你不需要这个检查。

理由:

当一个对象绑定到右值引用时,它是两件事情之一:

  1. 暂时的
  2. 主叫方希望您相信的一个对象是暂时的。

如果你有一个实际的临时对象的引用,那么根据定义,你有一个唯一的引用该对象。 它不可能被整个程序中的其他地方引用。 即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) 永远是个好主意。 如果在我自己的代码中find,我会认为它是一个性能错误,并修复它。 但是如果你想允许的话,意识到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; } 

上面的实现可以容忍自赋值,但是*thisother自variables赋值之后最终是一个零大小的数组,不pipe这个*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上的资源被立即释放,因此这种实现可能是有问题的。

第一个的成本是两个额外的商店。 第二个成本是一个testing和分支。 两者都有效。 两者都满足C ++ 11标准中表22 MoveAssignable要求的所有要求。 第三种模式也是模仿非内存资源关注。

所有这三种实现都可以根据硬件具有不同的成本:分支有多昂贵? 有很多寄存器还是很less?

外卖是自动分配,不像自我复制分配,不必保留现有的价值。

< / Update >

最后一个(希望)编辑灵感来自Luc Danton的评论:

如果您正在编写一个不直接pipe理内存的高级类(但可能有这样的基础或成员),那么移动赋值的最佳实现通常是:

 Class& operator=(Class&&) = default; 

这将移动分配每个基地和每个成员,并不包括this != &other检查。 假设在你的基地和成员之间不需要维护不variables,这将给你最高的性能和基本的例外安全。 对于要求强大的例外安全的客户,请将其指向strong_assign

首先,你的移动赋值操作符的签名是错误的。 由于移动从源对象中窃取资源,所以源必须是非常量的r值引用。

 Class &Class::operator=( Class &&rhs ) { //... return *this; } 

请注意,您仍然通过(非constl值引用返回。

对于任何一种types的直接分配,标准不是检查自我分配,而是确保自我分配不会导致崩溃和烧伤。 通常,没有人明确地做x = x或者y = std::move(y)调用,但是混叠,特别是通过多个函数,可能导致a = b或者c = std::move(d)成为自赋值。 明确检查自我赋值,即this == &rhs ,在真实时跳过该函数的肉是确保自分配安全性的一种方法。 但这是最坏的方法之一,因为它优化了(希望)罕见的情况,而对于更常见的情况(由于分支和可能的caching未命中),这是一种反优化。

现在,当(至less)其中一个操作数是一个直接临时对象时,你永远不会有一个自我分配的场景。 有些人主张假设这种情况,并优化代码,以至于假设错误时代码变得自杀愚蠢。 我说对用户倾销同一个对象是不负责任的。 我们不会为了复制分配而提出这个论点。 为什么要扭转移动职位的位置?

让我们举个例子,从另一个被访者那里改变:

 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; } 

这个副本分配处理自我分配,没有明确的检查。 如果源和目标大小不同,则在复制之前取消分配和重新分配。 否则,只是复制完成。 自我分配没有得到优化的path,当源和目标的大小开始相等时,自动分配会被转移到相同的path。 当两个对象是等价的(包括它们是同一个对象的时候)时,复制在技术上是不必要的,但是这是不进行相等性检查时的价格(数值方式或地址方式),因为所述检查本身是最浪费的的时间。 请注意,这里的对象自赋值会导致一系列元素级的自赋值; 元素types必须是安全的。

就像它的来源例子一样,这个拷贝提供了基本的例外安全保证。 如果您需要强有力的保证,那么请使用原来的“ 复制和交换”查询中的统一赋值运算符,该查询处理复制和移动赋值。 但这个例子的重点是要降低一个等级的安全性以获得速度。 (顺便说一下,我们假设单个元素的值是独立的,不存在限制某些值的不变约束。

我们来看看这个相同types的移动分配:

 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 ); } 

需要定制的可交换types应该在types相同的名称空间中有一个称为swap的双参数自由函数。 (命名空间限制允许非限定的调用交换工作。)容器types还应该添加一个公共swap成员函数来匹配标准容器。 如果未提供成员swap ,则可能需要将自由函数swap标记为可交换types的好友。 如果您自定义移动以使用swap ,那么您必须提供您自己的交换代码; 标准代码调用types的移动代码,这将导致移动自定义types的无限的相互recursion。

像析构函数一样,交换函数和移动操作应该永远不会抛出,如果可能的话,也可能标记为(在C ++ 11中)。 标准库types和例程对不可移动的移动types进行了优化。

这第一个版本的搬迁完成了基本合同。 源的资源标记被转移到目标对象。 由于源对象现在pipe理它们,旧的资源不会被泄漏。 源对象处于可用状态,可以对其进行进一步的操作,包括赋值和销毁。

请注意,由于swap呼叫是自动分配,因此此移动分配自动安全。 这也是强烈的例外安全。 问题是不必要的资源保留。 目的地的旧资源在概念上不再需要,但是在这里它们仍然只有这样才能使源对象保持有效。 如果源对象的计划销毁还有很长一段路要走,那么我们浪费资源空间,或者如果总资源空间有限,并且其他资源请求将在(新)源对象正式消亡之前发生,那么情况会更糟糕。

这个问题是什么导致有争议的当前大师关于自动定位在移动分配的build议。 没有挥之不去的资源编写移动分配的方式是这样的:

 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)起始行(如果你感觉不错)。

另一种方法是研究如何在不进行统一分配的情况下强制exception安全地进行复制分配,并将其应用于移动分配:

 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和保持这种方式。 然后,在获取原本由other拥有的资源的同时,将其旧资源temptemp 。 那么,当temp时候, this老的资源就会被杀死。

当自我分配发生时,清空othertemp清空this一点。 然后,目标对象在temp和交换时获取其资源。 temp的死亡声称是一个空洞的对象,实际上应该是一个空洞的对象。 this / other对象保持其资源。

只要移动build设和交换,移动分配也不应该丢失。 在自我分配过程中也是安全的成本是对低级types的更多指令,应该通过释放调用来消除。

我在那些想要自我分配安全的操作员的阵营,但不希望在operator=的实现中写自我分配检查。 实际上,我甚至不想实现operator= ,我希望默认行为能够“开箱即用”。 最好的特殊成员是那些免费来的。

这就是说,标准中的MoveAssignable需求描述如下(从17.6.3.1模板参数要求[utility.arg.requirements],n3290):

expression式返回types返回值后置条件
 t = rv T&tt相当于赋值前的rv值

占位符被描述为:“ t [是] T型可修改的左值;” 和“ rv是typesT的右值”。 请注意,这些要求是作为标准库模板参数的types,但是在标准的其他地方查看,我注意到在移动分配上的每个要求与此类似。

这意味着a = std::move(a)必须是“安全的”。 如果你需要的是一个身份testing(例如this != &other ),那就去做吧,否则你甚至不能把你的对象放到std::vector ! (除非你不使用那些需要MoveAssignable的成员/操作,但是不要这么做。)注意,在前面的例子中, a = std::move(a) ,那么this == &other将确实成立。

由于你现在的operator=函数是写的,因为你已经使右值引用参数为const ,所以你不可能“窃取”指针并改变传入的右值引用的值……你根本无法改变它,你只能读它。 我只会看到一个问题,如果你要开始在你的this对象中的指针等指针上delete ,就像你在一个正常的lvaue-reference operator=方法中一样,但是这种方式却不利于右值版本…换句话说,使用右值版本基本上可以执行与const -lvalue operator=方法相同的操作。

现在,如果你定义了operator=来取一个非const右值引用,那么我唯一能看到一个需要检查的方法就是把this对象传递给一个有意返回右值引用而不是临时值的函数。

例如,假设某人试图编写一个operator+函数,并且利用右值引用和左值引用的混合,以便在对象types的某些堆栈附加操作期间“防止”额外的临时对象被创build:

 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 } 

现在,从我所了解的右值引用,不鼓励上面的(即,你应该只是返回一个临时的,而不是右值引用),但是,如果有人仍然这样做,那么你会想要检查确保传入的右值引用没有引用与this指针相同的对象。

我的答案仍然是这个举动不一定是为了避免自我分配,而是有不同的解释。 考虑std :: unique_ptr。 如果我要实现一个,我会做这样的事情:

 unique_ptr& operator=(unique_ptr&& x) { delete ptr_; ptr_ = x.ptr_; x.ptr_ = nullptr; return *this; } 

如果你看看斯科特·迈耶斯 ( Scott Meyers)解释这一点, (如果你徘徊,为什么不做交换 – 它有一个额外的写)。 这对自我分配是不安全的。

有时这是不幸的。 考虑移出所有偶数的vector:

 src.erase( std::partition_copy(src.begin(), src.end(), src.begin(), std::back_inserter(even), [](int num) { return num % 2; } ).first, src.end()); 

这对于整数是可以的,但我不相信你可以用移动语义来做这种工作。

总结:将任务移动到对象本身并不好,你必须小心。

有一种情况,我可以想到(这== rhs)。 对于这个声明:Myclass obj; std :: move(obj)= std :: move(obj)