移动赋值操作符和‘ if (this! = & rhs)’

在类的赋值操作符中,你通常需要检查被赋值的对象是否是调用对象,这样你就不会把事情搞砸:

Class& Class::operator=(const Class& rhs) {
if (this != &rhs) {
// do the assignment
}


return *this;
}

对于移动赋值操作符也需要同样的东西吗?有没有 this == &rhs为真的情况?

? Class::operator=(Class&& rhs) {
?
}
49782 次浏览

在编写当前的 operator=函数时,由于已经创建了 rvalue-reference 参数 const,因此不可能“窃取”指针并更改传入的 rvalue 引用的值... ... 您根本无法更改它,只能从中读取。我只会看到一个问题,如果你开始调用 delete的指针等,在你的 this对象像你会在一个正常的 lvalue-reference operator=方法,但这有点违背了 rvalue-version... 也就是说,它似乎是多余的,使用 rvalue 版本基本上做相同的操作通常留给一个 const-lvalue operator=方法。

现在,如果您将 operator=定义为接受一个非 const的 rvalue 引用,那么我可以看到需要进行检查的唯一方法就是将 this对象传递给一个故意返回 rvalue 引用而不是临时引用的函数。

例如,假设有人试图编写一个 operator+函数,并使用 rvalue 引用和 lvalue 引用的混合,以“防止”在对象类型的堆叠加法操作期间创建额外的临时变量:

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
}

现在,根据我对右值引用的理解,不建议执行上述操作(即。应该只返回一个临时引用,而不是 rvalue 引用) ,但是,如果有人仍然这样做,那么您需要检查以确保传入的 rvalue 引用没有引用与 this指针相同的对象。

首先,复制和交换并不总是实现复制分配的正确方法。几乎可以肯定,在 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中的 move 赋值,应该是:

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

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

我个人认为你不需要这张支票。

理由:

当一个对象绑定到一个右值引用时,有两种情况:

  1. 暂时的。
  2. 调用者希望您相信的对象是临时的。

如果您有一个对实际临时对象的引用,那么根据定义,您就有一个对该对象的唯一引用。它不可能被整个程序中的任何其他地方引用。即 this == &temporary 是不可能的

如果你的委托人对你撒了谎并且向你保证你会得到一份临时工作而实际上你并没有,那么委托人就有责任确保你不必在乎。如果你真的想要小心谨慎,我相信这将是一个更好的实现:

Class&
Class::operator=(Class&& other)
{
assert(this != &other);
// ...
return *this;
}

也就是说。如果您传递了一个自我引用,那么这是客户机上应该修复的一个错误。

为了完整起见,下面是 dumb_array的 move 赋值操作符:

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示例中,如果我们简单地省略断言,或者将其约束为 move-from 情况,那么这将是完全无害的:

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的原始值是什么,*thisother最终都是一个零大小的数组。没关系。

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

上面的实现与复制赋值操作符一样,允许自赋值,将其设置为 no-op。这样也行。

dumb_array& operator=(dumb_array&& other)
{
swap(other);
return *this;
}

只有在 dumb_array没有持有应该“立即”销毁的资源时,上述操作才是可以的。例如,如果唯一的资源是内存,那么上面的方法就可以了。如果 dumb_array可能持有互斥锁或文件的打开状态,那么客户机可以合理地期望移动分配的 lhs 上的那些资源立即被释放,因此这个实现可能会有问题。

第一家的成本是两家额外的商店。第二种方法的成本是测试和分支。都能用。两者都满足 C + + 11标准中表22 MoveAssignable 需求的所有要求。第三部分还对非内存-资源关系进行了模块化研究。

根据硬件的不同,这三种实现的成本可能不同: 分支的成本有多高?有很多寄存器还是很少?

不同于自复制赋值,自动移动赋值不必保留当前值。

</Update>

最后一次编辑(希望如此)的灵感来自于 Luc Danton 的评论:

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

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

这将移动分配每个基地和每个成员依次,将不包括一个 this != &other检查。这将为您提供最高的性能和基本的异常安全性,前提是不需要在基础和成员之间维护不变量。对于需要强异常安全性的客户,请将他们指向 strong_assign

我支持那些想要自我分配安全操作符,但又不想在 operator=的实现中编写自我分配检查的人。事实上,我根本不想实现 operator=,我希望默认行为能够“立即开箱即用”。最好的特别会员是那些免费的会员。

也就是说,标准中出现的 MoveAssignable 需求描述如下(从17.6.3.1模板参数需求[ utility.arg.demand ] ,n3290开始) :

Expression  Return type Return value    Post-condition
t = rv      T&          t               t is equivalent to the value of rv before the assignment

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

这意味着 a = std::move(a)必须是“安全的”。如果您需要的是一个身份测试(例如 this != &other) ,那么就去做吧,否则您甚至不能将您的对象放入 std::vector!(除非您不使用那些确实需要 MoveAssignable 的成员/操作; 但是不要介意。)请注意,对于前面的示例 a = std::move(a),那么 this == &other将确实保持不变。

首先,移动赋值操作符的签名是错误的。由于 move 从源对象窃取资源,因此源必须是非 constr 值引用。

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

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

对于任何类型的直接分配,标准都不是检查自我分配,而是确保自我分配不会导致崩溃和烧毁。一般来说,没有人显式地执行 x = xy = std::move(y)调用,但别名,特别是通过多个函数,可能会导致 a = bc = std::move(d)成为自分配。自分配的显式检查,例如 this == &rhs,当为 true 时跳过函数的主要部分,这是确保自分配安全的一种方法。但这是最糟糕的方法之一,因为它优化了一个(希望是)罕见的情况,而对于更常见的情况却是一个反优化(由于分支和可能的缓存丢失)。

现在,当(至少)其中一个操作数是直接临时对象时,就不可能有自赋值场景。有些人主张假设这种情况,并为此优化代码,以至于当假设错误时,代码会变得愚蠢到自杀的地步。我认为向用户转储同一对象检查是不负责任的。我们没有为拷贝赋值提供这个参数; 为什么要反转 move 赋值的位置呢?

让我们举一个例子,从另一个被调查者:

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

这个拷贝分配可以很好地处理自分配,而不需要进行显式检查。如果源和目标大小不同,则在复制之前进行释放和重新分配。否则,只是复制完成。自分配不会得到一个优化的路径,它会在源和目标大小开始相等时被转储到相同的路径中。当两个对象是等价的时候(包括它们是同一个对象的时候) ,复制在技术上是不必要的,但是当不进行等价检查(按值或按地址)的时候,这就是代价,因为检查本身在大多数情况下是一种浪费。注意,这里的对象自赋值将导致一系列元素级别的自赋值; 元素类型必须是安全的。

与其源示例一样,此拷贝分配提供了基本的异常安全保证。如果需要强有力的保证,那么使用来自原始 复制和交换查询的统一赋值运算符,该运算符同时处理复制和移动赋值。但是这个例子的要点是减少一个等级的安全性来获得速度。(顺便说一句,我们假设单个元素的值是独立的; 没有不变约束限制某些值相对于其他值。)

让我们来看看同一类型的 move 分配:

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,那么您必须提供您自己的交换代码; 标准代码调用类型的 move 代码,这将导致移动自定义类型的无限相互递归。

与析构函数类似,交换函数和移动操作应该尽可能永远不抛出,并且可能标记为抛出(在 C + + 11中)。标准库类型和例程对不可抛出的移动类型进行了优化。

这第一个版本的 move-distribution 实现了基本的契约。源的资源标记被传输到目标对象。旧资源不会泄漏,因为源对象现在管理它们。并且源对象处于可用状态,可以对其应用进一步的操作,包括赋值和销毁。

注意,这个 move ——赋值对于自赋值是自动安全的,因为 swap调用是。它也是非常安全的。问题是不必要的资源保留。目的地的旧资源在概念上不再需要,但是在这里它们仍然存在,只是为了使源对象保持有效。如果源对象的预定销毁还有很长的路要走,那么我们就是在浪费资源空间,或者如果总资源空间有限,在(新的)源对象正式死亡之前会发生其他资源请求,那么情况就更糟了。

这个问题是什么造成了有争议的当前大师建议有关自我目标在调动-分配。在不使用延迟资源的情况下编写 move ——赋值的方法是这样的:

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

otherthis是不同的,other是清空的移动到 temp和保持这种方式。然后,this将其旧资源丢失给 temp,同时获得原来由 other持有的资源。然后当 temp这样做时,this的旧资源被杀死。

当自分配发生时,将 other清空到 temp也清空 this。然后,当 tempthis交换时,目标对象获得它的资源。temp的死亡声称一个空对象,这实际上应该是一个禁止操作。this/other对象保留其资源。

只要移动构造和交换也是如此,移动分配就应该是永远不抛出的。在自分配过程中保证安全的代价是在低级类型上增加一些指令,这些指令应该会被释放调用淹没。

我的答案仍然是,移动任务不一定要保存对自我分配,但它有一个不同的解释。考虑 std: : special _ ptr。如果我要实现一个,我会这样做:

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

如果你看看 Scott Meyers 在解释这个,他做了类似的事情。(如果你想知道为什么不做交换-它有一个额外的写)。这对自我分配来说不安全。

有时这是不幸的,考虑从向量中移出所有偶数:

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

这对于整数来说是可以的,但是我不相信您可以使用 move 语义来实现类似的功能。

总结一下: 将赋值移动到对象本身是不行的,你必须小心它。

小小的更新。

  1. 我不同意霍华德的观点,虽然这不是个好主意,但我还是觉得应该自己动手 分配“移出”对象应该工作,因为 swap(x, x)应该工作。算法喜欢这些东西!角落里的案子能成功总是好事。(我还没有看到一个案例,它不是免费的。但这并不意味着它不存在)。
  2. 这就是在 libc + + 中如何实现指定 only _ ptrs 的: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} 这是安全的自我移动任务。
  3. 核心指南 认为自我移动分配应该是可以的。

我能想到一种情况。 对于这个声明: 我类 obj; Move (obj) = std: : move (obj)