移动赋值运算符和`if(this!=&rhs)`

Set*_*gie 117 c++ move-semantics c++11 move-assignment-operator

在类的赋值运算符中,通常需要检查所分配的对象是否是调用对象,这样就不会搞砸了:

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

    return *this;
}
Run Code Online (Sandbox Code Playgroud)

移动赋值运算符需要相同的东西吗?是否有过这样的情况this == &rhs

? Class::operator=(Class&& rhs) {
    ?
}
Run Code Online (Sandbox Code Playgroud)

How*_*ant 135

哇,这里有很多要清理的东西......

首先,复制和交换并不总是实现复制分配的正确方法.几乎可以肯定dumb_array,这是次优解决方案.

使用复制和交换dumb_array是把与最大特征的最昂贵的操作在底部层的一个典型的例子.它非常适合希望获得最全功能并愿意支付性能损失的客户.他们得到了他们想要的东西.

但对于那些不需要最完整功能而是寻求最高性能的客户来说,这是灾难性的.对于他们来说,他们dumb_array只需重写另一部分软件,因为它太慢了.如果dumb_array设计不同,它可以满足两个客户,对任何一个客户都没有妥协.

满足两个客户端的关键是在最低级别构建最快的操作,然后在更高级别上添加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;
}
Run Code Online (Sandbox Code Playgroud)

说明:

在现代硬件上你可以做的更昂贵的事情之一就是去堆.你可以做的任何事情都是为了避免前往堆中,花费时间和精力.客户dumb_array可能希望经常分配相同大小的数组.当他们这样做时,你需要做的就是memcpy(隐藏在下面std::copy).您不希望分配相同大小的新数组,然后取消分配相同大小的旧数组!

现在为您的客户实际需要强大的异常安全性:

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    swap(lhs, rhs);
    return lhs;
}
Run Code Online (Sandbox Code Playgroud)

或者,如果你想利用C++ 11中的移动赋值应该是:

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    lhs = std::move(rhs);
    return lhs;
}
Run Code Online (Sandbox Code Playgroud)

如果dumb_array客户重视速度,他们应该打电话给operator=.如果他们需要强大的异常安全性,他们可以调用通用算法,这些算法可以在各种对象上运行,只需要实现一次.

现在回到原始问题(此时有一个类型o):

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

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

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

理由:

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

  1. 暂时的.
  2. 呼叫者希望您相信的对象是暂时的.

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

现在,如果您的客户对您撒谎并向您承诺,如果您不是,那么您将获得临时性,那么客户有责任确保您不必关心.如果你想要非常小心,我相信这将是一个更好的实现:

Class&
Class::operator=(Class&& other)
{
    assert(this != &other);
    // ...
    return *this;
}
Run Code Online (Sandbox Code Playgroud)

也就是说,如果你通过自参考,这是应该的固定客户端部分的错误.

为了完整性,这里有一个移动赋值运算符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;
}
Run Code Online (Sandbox Code Playgroud)

在移动分配的典型用例中,*this将是移动对象,因此delete [] mArray;应该是无操作.实现在nullptr上尽可能快地删除是至关重要的.

警告:

有些人会认为这swap(x, x)是一个好主意,或者只是一个必要的邪恶.而且,如果交换进入默认交换,则可能导致自动分配.

我不同意这swap(x, x)有史以来一个好主意.如果在我自己的代码中找到,我会认为它是性能错误并修复它.但是如果您想要允许它,请意识到swap(x, x)只有自动移动的assignemnet才能获得移动值.在我们的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;
}
Run Code Online (Sandbox Code Playgroud)

如果您自行分配两个移动(空)dumb_array,除了将无用的指令插入程序之外,您不会做任何不正确的事情.可以对绝大多数物体进行同样的观察.

<更新>

我更多地考虑了这个问题,并且稍微改变了我的立场.我现在认为作业应该容忍自我分配,但是复制作业和移动作业的后置条件是不同的:

副本分配:

x = y;
Run Code Online (Sandbox Code Playgroud)

一个人应该有一个y不应该改变价值的后置条件.当&x == &y那么这个后置条件转换为:自我拷贝赋值应该对价值没有影响x.

对于移动分配:

x = std::move(y);
Run Code Online (Sandbox Code Playgroud)

一个应该有一个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
}
Run Code Online (Sandbox Code Playgroud)

以上作品,只要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;
}
Run Code Online (Sandbox Code Playgroud)

上述实施容忍自我分配,但是*thisother最终成为自招分配后一个零大小的数组,不管是什么的原值*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;
}
Run Code Online (Sandbox Code Playgroud)

上述实现允许自我赋值与复制赋值运算符相同,方法是使其成为无操作.这也没关系.

dumb_array& operator=(dumb_array&& other)
{
    swap(other);
    return *this;
}
Run Code Online (Sandbox Code Playgroud)

只有在dumb_array没有"应该立即"销毁的资源时,上述情况才可以.例如,如果唯一的资源是内存,上面就可以了.如果dumb_array可能存在互斥锁或文件的打开状态,则客户端可以合理地期望移动分配的lhs上的那些资源立即被释放,因此这种实现可能是有问题的.

第一个的成本是两个额外的商店.第二个成本是测试和分支.两者都有效.两者都满足C++ 11标准中表22 MoveAssignable要求的所有要求.第三个也模拟非内存资源问题.

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

外卖是自动分配,与自我复制分配不同,不必保留当前值.

</更新>

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

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

Class& operator=(Class&&) = default;
Run Code Online (Sandbox Code Playgroud)

这将依次移动分配每个基地和每个成员,并且不包括this != &other支票.这将为您提供最高性能和基本的异常安全性,假设您的基地和成员之间不需要保持不变量.对于要求强大异常安全性的客户,请指出strong_assign.

  • 我不知道如何看待这个答案.它使得看起来像实现这样的类(非常明确地管理它们的内存)是常见的事情.确实,当你*写*这样的类时,必须非常小心异常安全保证并找到界面的最佳位置,简洁但方便,但问题似乎是要求一般建议. (6认同)
  • 倾向于提出移动 - 从 - 自我分配应该**断言 - 失败或产生"未指明"结果的建议.自我分配实际上是最容易实现的最简单的案例**.如果您的类在`std :: swap(x,x)`上崩溃,那么我为什么要相信它能正确处理更复杂的操作呢? (3认同)

CTM*_*ser 11

首先,你得到了移动赋值运算符的签名错误.由于移动从源对象窃取资源,因此源必须是非constr值引用.

Class &Class::operator=( Class &&rhs ) {
    //...
    return *this;
}
Run Code Online (Sandbox Code Playgroud)

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

对于任何类型的直接分配,标准不是检查自我分配,而是确保自我分配不会导致崩溃和烧毁.通常,没有人明确地做x = xy = std::move(y)调用,但是别名,特别是通过多个功能,可能导致a = bc = 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;
}
Run Code Online (Sandbox Code Playgroud)

此复制分配可以优雅地处理自我分配,而无需进行显式检查.如果源和目标大小不同,则在复制之前重新分配和重新分配.否则,只需复制即可.自我赋值不会获得优化路径,它会被转储到与源和目标大小开始时相同的路径中.当两个对象相同时(包括它们是同一个对象时),技术上不需要复制,但这是不进行相等检查(价值方式或地址方式)时的价格,因为所述检查本身是最浪费的的时间.请注意,此处的对象自我赋值将导致一系列元素级别的自我分配; 元素类型必须是安全的.

与其源示例一样,此复制分配提供基本的异常安全保证.如果您需要强保证,请使用原始复制和交换查询中的统一分配运算符,该查询处理复制和移动分配.但这个例子的重点是将安全性降低一级以获得速度.(顺便说一句,我们假设各个元素的值是独立的;没有不变约束限制某些值与其他元素相比.)

让我们看看这个相同类型的移动分配:

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 ); }
Run Code Online (Sandbox Code Playgroud)

需要自定义的可交换类型应该具有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;
    }
    //...
};
Run Code Online (Sandbox Code Playgroud)

源被重置为默认条件,而旧的目标资源被销毁.在自我分配的情况下,你当前的对象最终会自杀.围绕它的主要方法是用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;
    }
    //...
};
Run Code Online (Sandbox Code Playgroud)

什么时候otherthis不同的,other被移动到那里temp并且保持这种方式.然后在获取原始this资源的temp同时丢失其旧资源other.那么旧资源this何时被杀死temp.

当自赋值发生,排空other,以temp清空this为好.然后,目标对象获得资源回来时tempthis交换.temp索赔的死亡是一个空洞的对象,实际上应该是无操作的.该this/ other对象保持其资源.

只要移动构造和交换也移动,移动分配应该永远不会丢弃.在自我分配期间也是安全的成本是低级别类型的一些指令,应该通过释放调用来淹没.

  • 您的第二个代码示例,即没有自我分配检查的复制赋值运算符是错误的.如果源和目标范围重叠(包括它们重合的情况),`std :: copy`会导致未定义的行为.见C++ 14 [alg.copy]/3. (3认同)

Luc*_*ton 6

我是那些想要自我赋值安全运算符的人,但不想在实现中编写自我赋值检查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[是一个]可修改的T型左值;" 并且" rv是T型的右值;".请注意,那些是用作标准库模板的参数的类型的要求,但是在标准的其他地方我注意到移动分配的每个要求都与此类似.

这意味着a = std::move(a)必须"安全".如果您需要的是身份测试(例如this != &other),那就去吧,否则您甚至无法将对象放入其中std::vector!(除非你不使用那些需要MoveAssignable的成员/操作;但是没关系.)请注意,使用前面的例子a = std::move(a),那么this == &other确实会成立.