9

这与Matthieu M. 提供的关于如何使用带有 + 运算符重载的移动语义的答案有关(通常,运算符不直接重新分配回左侧参数)。

他建议实现三个不同的重载:

inline T operator+(T left, T const& right) { left += right; return left; }
inline T operator+(T const& left, T right) { right += left; return right; } // commutative
inline T operator+(T left, T&& right) { left += right; return left; } // disambiguation

数字 1 和 3 是有道理的,但我不明白目的 2 的作用。该评论建议进行交换处理,但似乎 1 和 2 将是互斥的(即在模棱两可的情况下实现这两个结果)

例如,所有 3 个都已实现:

T a, b, c;
c = a + b;

编译器输出:

1> 错误 C2593: 'operator +' 不明确
1> 可以是 'T operator +(const T &,T)'
1> 或 'T 运算符 +(T,const T &)'
1> 在尝试匹配参数列表 '(T, T)' 时

删除 1 或 2 并且程序按预期工作。由于 1 是一般情况,而 2 只能与可交换运算符一起正常工作,所以我不明白为什么会使用 2。有什么我想念的吗?

4

2 回答 2

12

我认为您没有遗漏任何东西-您问题中的代码确实很麻烦。他的回答的前面部分是有道理的,但是在“四个期望的情况”和实际示例之间丢失了一些东西。

这可能会更好:

inline T operator+(T left, T const& right) { left += right; return left; }
inline T operator+(const T& left, T&& right) { right += left; return right; }

这实现了规则:制作 LHS 的副本(最好通过移动构造),除非 RHS 无论如何都会过期,在这种情况下修改它。

对于非交换运算符,省略第二个重载,或者提供不委托给复合赋值的实现。

如果您的班级内部嵌入了重量级资源(因此无法有效移动),您将希望避免按值传递。丹尼尔在他的回答中提出了一些很好的观点。但不要T&&像他建议的那样返回,因为那是一个悬而未决的参考。

于 2013-04-21T21:40:33.610 回答
6

关于此答案的重要更新/警告!

实际上一个令人信服的例子,它在合理的真实世界代码中默默地创建了一个悬空引用,如下所示。请使用其他答案的技术来避免此问题,即使以创建一些额外的临时对象为代价。我将保留此答案的其余部分,以供将来参考。


可交换情况的正确重载是:

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

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

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

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

为什么会这样,它是如何工作的?首先,请注意,如果您将右值引用作为参数,您可以修改并返回它。它来自的表达式需要保证右值在完整表达式结束之前不会被破坏,包括operator+. 这也意味着operator+可以简单地返回右值引用,因为调用者需要在表达式被完全评估并且临时变量(ravlues)被破坏之前使用operator+(它是同一表达式的一部分)的结果。

第二个重要的观察是,这如何节省更多的临时和移动操作。考虑以下表达式:

T a, b, c, d; // initialized somehow...

T r = a + b + c + d;

与上述内容,它相当于:

T t( a );    // T operator+( const T& lhs, const T& rhs );
t += b;      // ...part of the above...
t += c;      // T&& operator+( T&& lhs, const T& rhs );
t += d;      // T&& operator+( T&& lhs, const T& rhs );
T r( std::move( t ) ); // T&& was returned from the last operator+

将此与其他方法发生的情况进行比较:

T t1( a );   // T operator+( T lhs, const T& rhs );
t1 += b;     // ...part of the above...
T t2( std::move( t1 ) ); // t1 is an rvalue, so it is moved
t2 += c;
T t3( std::move( t2 ) );
t3 += d;
T r( std::move( t3 );

这意味着您仍然有三个临时对象,尽管它们被移动而不是复制,但上述方法在完全避免临时对象方面效率更高。

有关完整的库,包括对 的支持noexcept,请参阅df.operators。在那里,您还将找到非交换案例和混合类型操作的版本。


这是一个完整的测试程序来测试它:

#include <iostream>
#include <utility>

struct A
{
  A() { std::cout << "A::A()" << std::endl; }
  A( const A& ) { std::cout << "A::A(const A&)" << std::endl; }
  A( A&& ) { std::cout << "A::A(A&&)" << std::endl; }
  ~A() { std::cout << "A::~A()" << std::endl; }

  A& operator+=( const A& ) { std::cout << "+=" << std::endl; return *this; }
};

// #define BY_VALUE
#ifdef BY_VALUE
A operator+( A lhs, const A& rhs )
{
  lhs += rhs;
  return lhs;
}
#else
A operator+( const A& lhs, const A& rhs )
{
  A nrv( lhs );
  nrv += rhs;
  return nrv;
}

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

int main()
{
  A a, b, c, d;
  A r = a + b + c + d;
}
于 2013-04-21T22:41:01.837 回答