应该使用哪个:移动赋值运算符还是复制赋值运算符?

12

我似乎不理解为什么要使用移动赋值运算符

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

另外,复制赋值运算符:

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

移动赋值运算符只接受r-value reference参数。


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

复制赋值运算符中,可以使用复制构造函数移动构造函数来构造other(如果使用rvalue初始化other,则可以使用move-constructor——如果定义了move-constructor)。

如果使用复制构造函数,我们将进行1次复制,而这个复制是无法避免的。

如果使用移动构造函数,则性能/行为与第一个重载产生的效果相同。

我的问题是:

1- 为什么要实现移动赋值运算符

2- 如果other是从r-value构造的,则编译器会选择调用哪个赋值运算符?为什么?


  1. 更少的工作量
  2. 这可能会产生歧义。
- Kerrek SB
所以你的意思是对于第2个问题,将调用第一个重载吗? - Kam
CLASSA & operator=(CLASSA && other); 是一个移动赋值运算符。不确定这如何改变您所询问的内容。复制赋值运算符接受CLASSAconst CLASSA& - Radiodef
@Kam:不行。模糊重载意味着当您尝试使用该重载时,重载决策失败,进而使程序形式不正确。 - Kerrek SB
性能/行为将不完全相同。如果您通过r值引用传递,可能会少一个移动操作。这是否重要取决于移动CLASSA的成本如何。 - Chris Drew
Herb Sutter在CppCon的“回归基础!现代C++风格的基本要素”演讲中详细阐述了这一点。链接:http://www.youtube.com/watch?v=xnqTKD8uD64&t=51m5s - Chris Drew
2个回答

10

您没有进行类比比较

如果您正在编写像std::unique_ptr这样的移动类型,那么移动赋值运算符将是您唯一的选择。

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

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

请注意,将您建议的两个重载都放在一个类中不是一个选项,因为它会产生歧义。

选项1是传统的C++98选项,在大多数情况下表现良好。但是,如果您需要优化r-values,则可以考虑选项2并添加移动赋值运算符。

有时候我们会考虑选项3,即传递值然后移动,我想这就是您所建议的。在这种情况下,您只需编写一个赋值运算符。它接受l-values,并且仅以一个额外的移动作为代价接受r-values,许多人会提倡这种方法。

然而,在CppCon 2014中Herb Sutter在他的"回到基础!现代C++风格的基本要素"演讲中指出,这个选项有问题并且可能会更慢。在l-values的情况下,它将执行无条件的复制,并且不会重用任何现有的容量。他提供了支持他观点的数字。唯一的例外是构造函数,因为没有现有的容量可供重用,而且通常有许多参数,因此传递值可以减少所需的重载数量。
因此,我建议您从Option 1开始,如果需要优化r-values,则转向Option 2。

2
显然,这两个重载函数不等价:
  1. 只有采用右值引用的赋值运算符可以处理表达式右侧为右值的情况。对于左值,需要另一个重载函数,例如采用 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)

2
Herb Sutter指出,在这种情况下使用传值方式可能比传递const引用方式慢得多,因为它会进行无条件的复制并且不会重用现有容量。 - Chris Drew

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接