C++ 是否需要重载运算符的特殊右值变体?

C++ Is a special rvalue-variant of an overloaded operator necessary?

在像 Operator Overloading 这样的问题+答案中,指出重载 operator+ 这样的二元运算符的最佳方法是:

class X {
  X& operator+=(const X& rhs)
  {
    // actual addition of rhs to *this
    return *this;
  }
};
inline X operator+(X lhs, const X& rhs)
{
  lhs += rhs;
  return lhs;
}
因此,

operator+ 本身按值取 lhs,按常量引用取 rhs,按值取 returns 改变的 lhs

我无法理解如果使用右值 lhs 调用这里会发生什么:这仍然是需要的单一定义吗(编译器是否会优化参数的移动和 return 值),或者添加与右值引用一起使用的运算符的第二个重载版本是否有意义?

编辑:

有趣的是,在Boost.Operators中,他们谈到了这个实现:

T operator+( const T& lhs, const T& rhs )
{
   T nrv( lhs );
   nrv += rhs;
   return nrv;
}

允许命名 Return 值优化,但默认情况下不使用它,因为:

Sadly, not all compiler implement the NRVO, some even implement it in an incorrect way which makes it useless here

这些新信息不足以让我提供完整的答案,但它可能会让其他一些聪明的人得出一个包罗万象的结论。

如果以右值引用作为第一个参数调用运算符,例如当使用 std::move 或函数调用的直接结果时,将调用 lhs 的移动构造函数。不需要采用右值引用的额外重载。

这个签名:

inline X operator+(X lhs, const X& rhs)

允许右值和左值作为操作的 left-hand 端。左值将被复制到 lhs,xvalues 将被移动到 lhs,而 prvalues 将被直接初始化到 lhs

按值取 lhs 和按 const&lhs 之间的区别在我们链接多个 + 操作时体现出来。让我们做一个 table:

+====================+==============+=================+
|                    | X const& lhs | X lhs           |
+--------------------+--------------+-----------------+
| X sum = a+b;       | 1 copy       | 1 copy, 1 move  |
| X sum = X{}+b;     | 1 copy       | 1 move          |
| X sum = a+b+c;     | 2 copies     | 1 copy, 2 moves |
| X sum = X{}+b+c;   | 2 copies     | 2 moves         |
| X sum = a+b+c+d;   | 3 copies     | 1 copy, 3 moves |
| X sum = X{}+b+c+d; | 3 copies     | 3 moves         |
+====================+==============+=================+                

将第一个参数 const& 用于复制的数量。每个操作都是一个副本。按值缩放第一个参数的副本数。每个操作只是一个移动但是第一个参数是左值意味着额外的副本(或xvalues的额外移动)。

如果你的类型移动起来并不便宜——对于那些移动和复制是等价的情况——那么你想通过 const& 获取第一个参数,因为它至少和另一种情况一样好没有理由大惊小怪。

但如果搬家更便宜,您实际上可能想要两个重载:

X operator+(X const& lhs, X const& rhs) {
    X tmp(lhs);
    tmp += rhs;
    return tmp;
}

X operator+(X&& lhs, X const& rhs) {
    lhs += rhs;
    return std::move(lhs);
}

这将对所有中间临时对象使用移动而不是副本,但会在第一个时为您节省一个移动。不幸的是,最好的解决方案也是最冗长的。