使用哪种:移动分配运算符与复制分配运算符

Which to use: move assignment operator vs copy assignment operator

本文关键字:分配 运算符 复制 移动      更新时间:2023-10-16

我似乎不明白为什么要使用move assignment operator:

CLASSA & operator=(CLASSA && other); //move assignment operator

结束,copy assignment operator:

CLASSA & operator=(CLASSA  other); //copy assignment operator

move assignment operator仅采用r-value reference,例如

CLASSA a1, a2, a3;
a1 = a2 + a3;

copy assignment operator中,other可以是使用copy constructormove constructor的构造函数(如果other是用右值初始化的,那么它可以是移动构造的——如果定义了move-constructor——)。

如果是copy-constructed,我们将进行1次复制,该复制无法避免。

如果是move-constructed,则性能/行为与第一次过载产生的性能/行为相同。

我的问题是:

1-为什么要实现move assignment operator

2-如果other是由r值构造的,那么编译器会选择调用哪个assignment operator?为什么?

您不是在比较同类

如果您正在编写像std::unique_ptr这样的仅移动类型,那么移动分配操作符将是您唯一的选择。

更典型的情况是,你有一个可复制的类型,在这种情况下,我认为你有三种选择。

  1. T& operator=(T const&)
  2. T& operator=(T const&)T& operator=(T&&)
  3. T& operator=(T)和移动

请注意,在一个类中同时拥有您建议的两个重载并不是一个选项,因为这是不明确的。

选项1是传统的C++98选项,在大多数情况下都表现良好。但是,如果需要针对r值进行优化,可以考虑选项2并添加一个移动赋值操作符。

考虑选项3并按价值传递,然后移动是很诱人的,我认为这就是你的建议。在这种情况下,您只需要编写一个赋值运算符。它接受l值,只需付出一次额外的行动就可以接受r值,许多人会提倡这种方法。

然而,Herb Sutter在2014年CppCon的"回归基础!现代C++风格的要点"演讲中指出,这种选择是有问题的,而且可能会慢得多。在l值的情况下,它将执行无条件复制,并且不会重用任何现有容量。他提供了数字来支持他的说法。唯一的例外是构造函数,那里没有可重用的现有容量,并且您通常有许多参数,因此传递值可以减少所需的重载次数。

因此,如果需要优化r值,我建议您从选项1开始,然后转到选项2。

显然,这两个重载是不等价的:

  1. 接受右值引用的赋值运算符仅适用于表达式右侧的右值。为了支持lvalues,对于可复制类型,还需要另一个重载,例如使用T const&。当然,对于仅移动类型(如std::unique_ptr<T>),定义此赋值运算符是合适的选择
  2. 假定所讨论的类型是可复制和可移动构造的,则取值的赋值运算符涵盖了右值和左值赋值。它的规范实现是调用swap(),用右侧的状态替换对象的状态。它的优点是,论点的复制/移动结构通常可以被忽略

无论如何,您都不希望在一个类中同时包含两个重载!当从左值赋值时,显然会选择采用值的版本(另一个选项不可行)。然而,当分配右值时,两个赋值运算符都是可行的,即,会有歧义。这可以很容易地通过尝试编译以下代码来验证:

struct foo
{
    void operator=(foo&&) {}
    void operator=(foo) {}
};
int main()
{
    foo f;
    f = foo();
}

要分别处理移动和复制构造,可以使用T&&T const&作为参数定义一对赋值运算符。然而,这导致必须实现两个版本的基本相同的复制分配,而只有一个T作为参数只需要实现一个复制分配。

因此,有两个明显的选择:

  1. 对于只移动类型,您可以定义T::operator= (T&&)
  2. 对于可复制类型,您可以定义T::operator=(T)