C++移动分配可防止复制交换习惯用法

C++ move-assignment prevents copy-swap idiom

本文关键字:交换 习惯 惯用法 复制 可防止 移动 分配 C++      更新时间:2023-10-16

在C++中,复制交换习语通常实现如下:

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 中的最后三个函数调用现在已经下降到单个函数调用。这是意料之中的,因为我们不再按值传递赋值运算符参数,因此那里不会发生构造。

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

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

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

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

C& operator=(C rhs) noexcept;

替换对的方法

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

C& 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