【问题标题】:C++ move-assignment prevents copy-swap idiomC++ 移动赋值防止复制交换习语
【发布时间】:2018-04-12 12:13:39
【问题描述】:

在 C++ 中,copy-swap 习惯用法通常是这样实现的:

C& operator=(C rhs)
{
    swap(*this, rhs);
    return *this;
}

现在,如果我想添加一个移动赋值运算符,它应该是这样的:

C& operator=(C&& rhs)
{
    swap(*this, rhs);
    return *this;
}

但是,这会导致应该调用哪个赋值运算符的歧义,编译器理所当然地抱怨它。所以我的问题如下:如果我想支持复制交换习语和移动赋值语义,我应该怎么做?

或者这是一个非问题,因为拥有移动复制构造函数和复制交换习语,一个人并没有真正从拥有移动赋值运算符中受益?

在问了这个问题之后,我编写了一个代码来演示移动赋值可能会导致比复制交换习语更少的函数调用。首先让我介绍一下我的复制交换版本。请多多包涵;它看起来像一个长而简单的例子:

#include <algorithm>
#include <iostream>
#include <new>

using namespace std;

bool printOutput = false;

void* operator new(std::size_t sz)
{
    if (printOutput)
    {
        cout << "sz = " << sz << endl;
    }

    return std::malloc(sz);
}

class C
{
    int* data;

    public:

    C() : data(nullptr)
    {
        if (printOutput)
        {
            cout << "C() called" << endl;
        }
    }

    C(int data) : data(new int)
    {
        if (printOutput)
        {
            cout << "C(data) called" << endl;
        }

        *(this->data) = data;
    }

    C(const C& rhs) : data(new int)
    {
        if (printOutput)
        {
            cout << "C(&rhs) called" << endl;
        }

        *data = *(rhs.data);
    }

    C(C&& rhs) : C()
    {
        if (printOutput)
        {
            cout << "C(&&rhs) called" << endl;
        }

        swap(*this, rhs);
    }

    C& operator=(C rhs)
    {
        if (printOutput)
        {
            cout << "operator= called" << endl;
        }

        swap(*this, rhs);

        return *this;
    }

    C operator+(const C& rhs)
    {
        C result(*data + *(rhs.data));

        return result;
    }

    friend void swap(C& lhs, C& rhs);

    ~C()
    {
        delete data;
    }
};

void swap(C& lhs, C& rhs)
{
    std::swap(lhs.data, rhs.data);
}

int main()
{
    C c1(7);
    C c2;

    printOutput = true;

    c2 = c1 + c1;

    return 0;
}

我已经使用 -fno-elide-constructors 选项使用 g++ 编译了它,因为我想查看无优化行为。结果如下:

sz = 4
C(data) called   // (due to the declaration of result)
C() called       // (called from the rvalue copy-constructor)
C(&&rhs) called  // (called due to copy to return temporary)
C() called       // (called from the rvalue copy-constructor)
C(&&rhs) called  // (called due to pass-by-value in the assignment operator)
operator= called

现在,如果我选择不在赋值运算符中使用复制交换习语,我会得到这样的结果:

C& operator=(const C& rhs)
{
    if (printOutput)
    {
        cout << "operator=(const C&) called" << endl;
    }

    if (this != &rhs)
    {
        delete data;

        data = new int;
        *data = *(rhs.data);
    }

    return *this;
}

这使我可以使用如下的移动赋值运算符:

C& operator=(C&& rhs)
{
    if (printOutput)
    {
        cout << "operator=(C&&) called" << endl;
    }

    swap(*this, rhs);

    return *this;
}

现在,在其他一切都相同的情况下,我得到以下输出:

sz = 4
C(data) called        // (due to the declaration of result)
C() called            // (called from the rvalue copy-constructor)
C(&&rhs) called       // (called due to copy to return temporary)
operator=(C&&) called // (move-assignment)

如您所见,这会减少函数调用。实际上,copySwapIdiom 中的最后三个函数调用现在已经下降到一个函数调用。这是意料之中的,因为我们不再按值传递赋值运算符参数,因此那里不会发生构造。

但是,我并没有从赋值运算符中复制交换习语的美感中受益。非常感谢任何见解。

【问题讨论】:

  • “通常是这样实现的” - 为了避免编写两个重载,一个用于移动右值,一个用于复制 lvlaues...
  • 简答:使用移动语义。这就是它的用途。
  • C&amp; operator=(C rhs) 是一个移动赋值运算符。按值传递的对象可以从右值初始化。您无需添加任何其他内容
  • 我收到的最佳答案是来自 M.M. 的评论。对我错误创建的答案。我在这里复制/粘贴它,所以它不会丢失:“这应该是问题的一部分。虽然我会为你节省一些时间并说:是的,复制交换成语涉及更多的构造函数调用而不是单独的复制-赋值和移动赋值运算符。您需要在性能与代码简单性之间进行权衡。这将在 stackoverflow.com/questions/3279543 – MM“上进行更深入的讨论”

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


【解决方案1】:

如果您提供有效的移动构造函数,则实际上不需要实现移动赋值运算符。

class Foo
{
public:
   explicit Foo(Bar bar)
       : bar(bar)
    { }

    Foo(const Foo& other)
        : bar(other.bar)
    { }

    Foo(Foo&& other)
        : bar(other.bar)
    { }

    // other will be initialized using the move constructor if the actual
    // argument in the assignment statement is an rvalue
    Foo& operator=(Foo other)
    {
        std::swap(bar, other.bar);
        return *this;
    }

【讨论】:

  • 事实上,这是使用复制和交换的主要原因之一:您将所有复杂的行为包含在尽可能少的函数中,而不必将其复制到多个函数中。
【解决方案2】:

此处复制交换习语背后的动机是将复制/移动工作转发给构造函数,这样您就不会为构造函数和赋值运算符重复工作。也就是说,

C& operator=(C rhs) noexcept;

表示替换对

C& operator=(const C& rhs);
C& operator=(C&& rhs) noexcept;

C&amp; operator=(C rhs) noexcept; 是执行复制还是移动赋值取决于rhs 的构造方式。例如,

a = std::move(b); // rhs is move-constructed from r-value std::move(b), and thus move-assignment
c = d;            // rhs is copy-constructed from l-value d, and thus copy-assignment

【讨论】:

  • 我没有得到这个。程序如何知道对象是如何构造的?
  • @EduardRostomyan 更新了答案以澄清。
猜你喜欢
  • 1970-01-01
  • 2020-05-27
  • 2015-07-21
  • 1970-01-01
  • 2011-10-28
  • 1970-01-01
  • 2013-09-21
  • 2013-08-10
  • 2014-01-02
相关资源
最近更新 更多