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

时间:2012-02-17 02:41:56

标签: c++ c++11 move-semantics move-assignment-operator

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

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

    return *this;
}

移动赋值运算符需要相同的东西吗?是否存在this == &rhs为真的情况?

? Class::operator=(Class&& rhs) {
    ?
}

6 个答案:

答案 0 :(得分:128)

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

首先,Copy and Swap并不总是实现复制分配的正确方法。几乎可以肯定,在dumb_array的情况下,这是次优解决方案。

使用Copy and Swap用于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;
}

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

我的个人意见不是,你不需要这张支票。

理由:

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

  1. 暂时的。
  2. 来电者希望您相信的一个对象是暂时的。
  3. 如果您对作为实际临时对象的对象有引用,则根据定义,您具有对该对象的唯一引用。它不可能被整个程序中的任何其他地方引用。即this == &temporary 无法

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

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

    即。如果您 传递了自引用,则这是客户端应该修复的错误。

    为了完整性,这里是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)仅对移动的值进行自动移动 - 分配。在我们的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;
    }
    

    如果你自行分配两个移动的(空的)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在自动分配后最终成为零大小的数组,无论*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不包含应该被破坏的资源时,上述情况才会出现,#34;立即#34;例如,如果唯一的资源是内存,上面就可以了。如果dumb_array可能存在互斥锁或文件的打开状态,则客户端可以合理地期望移动分配的lhs上的那些资源立即被释放,因此这种实现可能会有问题。

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

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

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

    <强> < /更新>

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

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

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

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

答案 1 :(得分:11)

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

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

请注意,您仍然可以通过(非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;
}

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

与其源示例一样,此复制分配提供基本的异常安全保证。如果您需要强保证,请使用原始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标记为可交换类型的朋友。如果您自定义移动以使用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;
    }
    //...
};

otherthis不同时,移动到other会清空temp并保持这种状态。然后this在获取temp最初持有的资源时,将其旧资源丢失至other。然后,当this执行时temp的旧资源被杀死。

当自我分配发生时,other清空temp也会清空this。然后,当tempthis交换时,目标对象会返回其资源。 temp的死亡声称是一个空洞的对象,实际上应该是一个无操作的对象。 this / other对象保留其资源。

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

答案 2 :(得分: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确实会成立。

答案 3 :(得分:2)

由于您编写了当前的operator=函数,因为您已经创建了rvalue-reference参数const,所以无法“窃取”指针并更改传入rvalue的值参考...你根本无法改变它,你只能从它读取。我只会在你delete对象的指针等上开始调用this时遇到问题,就像在正常的lvaue-reference operator=方法中那样,但是那种击败rvalue-version的点......也就是说,使用rvalue版本基本上执行通常留给const - 左值operator=方法的相同操作似乎是多余的。

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

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

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引用),但是,如果有人仍然这样做,那么你想要检查以确保传入的rvalue-reference没有引用与this指针相同的对象。

答案 4 :(得分:1)

我的回答仍然是移动任务不一定要自救,但它有不同的解释。考虑std :: unique_ptr。如果我要实现一个,我会做这样的事情:

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

如果你看Scott Meyers explaining this他做了类似的事情。 (如果你徘徊为什么不做交换 - 它有一个额外的写)。这对于自我分配来说是不安全的。

有时这很不幸。考虑从矢量中移出所有偶数:

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

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

总结:移动对象本身的分配是不正确的,你必须注意它。

小更新。

  1. 我不赞同霍华德,这是一个坏主意,但仍然 - 我认为自我感动 “移出”对象的分配应该有效,因为swap(x, x)应该有效。算法喜欢这些东西!当一个角落案件正常工作时,它总是很好。 (而且我还没有看到它不是免费的情况。并不意味着它不存在)。
  2. 这是在libc ++中实现分配unique_ptrs的方法: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} 自行转让是安全的。
  3. Core Guidelines认为自行移动分配应该没问题。

答案 5 :(得分:0)

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