在重载算法中移动语义和Rvalue-Reference

Tie*_*inë 12 c++ rvalue-reference move-semantics c++11

我正在使用C++编写一个小型数值分析库.我一直在尝试使用最新的C++ 11功能实现,包括移动语义.我理解以下帖子中的讨论和最佳答案:C++ 11 rvalues和移动语义混淆(return语句),但有一种情况我仍然试图包围我的头脑.

我有一个类,称之为T,它配备了重载运算符.我也有复制和移动构造函数.

T (const T &) { /*initialization via copy*/; }
T (T &&) { /*initialization via move*/; }
Run Code Online (Sandbox Code Playgroud)

我的客户端代码大量使用运算符,所以我试图确保复杂的算术表达式从移动语义中获得最大的好处.考虑以下:

T a, b, c, d, e;
T f = a + b * c - d / e;
Run Code Online (Sandbox Code Playgroud)

没有移动语义,我的操作符每次都使用复制构造函数创建一个新的局部变量,所以总共有4个副本.我希望通过移动语义,我可以将其减少到2个副本加上一些动作.在括号中:

T f = a + (b * c) - (d / e);
Run Code Online (Sandbox Code Playgroud)

每个(b * c)并且(d / e)必须以通常的方式创建临时副本,但是如果我可以利用其中一个临时工具仅用移动来累积剩余的结果,那将是很好的.

使用g ++编译器,我已经能够做到这一点,但我怀疑我的技术可能不安全,我想完全理解为什么.

以下是加法运算符的示例实现:

T operator+ (T const& x) const
{
    T result(*this);
    // logic to perform addition here using result as the target
    return std::move(result);
}
T operator+ (T&& x) const
{
    // logic to perform addition here using x as the target
    return std::move(x);
}
Run Code Online (Sandbox Code Playgroud)

如果没有调用std::move,则只const &调用每个运算符的版本.但是当std::move如上使用时,使用&&每个运算符的版本执行后续算法(在最内层表达式之后).

我知道RVO可以被抑制,但在计算成本非常高的现实问题上,似乎收益略微超过了RVO的缺乏.也就是说,超过数百万次计算,当我加入时,我确实得到了非常小的加速std::move.虽然说实话,但没有足够快.我真的只想完全理解这里的语义.

是否有一位C++ Guru愿意花时间以简单的方式解释我是否以及为何使用std :: move在这里是一件坏事?提前谢谢了.

Dav*_*eas 8

您应该更喜欢将运算符重载为自由函数以获得完全类型对称(可以在左侧和右侧应用相同的转换).这使得你在问题中遗漏的内容更加明显.将您的操作员重新设置为您提供的免费功能:

T operator+( T const &, T const & );
T operator+( T const &, T&& );
Run Code Online (Sandbox Code Playgroud)

但是你没有提供一个处理左侧是临时的版本:

T operator+( T&&, T const& );
Run Code Online (Sandbox Code Playgroud)

并且当两个参数都是rvalues时,为了避免代码中出现歧义,您需要提供另一个重载:

T operator+( T&&, T&& );
Run Code Online (Sandbox Code Playgroud)

常见的建议是实现+=作为修改当前对象的成员方法,然后编写operator+为修改接口中相应对象的转发器.

我没有真正想过这么多,但可能有一个替代方法T(没有r/lvalue引用),但我担心它不会减少你需要提供的重载次数,以便operator+在所有情况下都能提高效率.


And*_*ard 5

以其他人所说的为基础:

  • std::movein 的调用T::operator+( T const & )是不必要的,可以防止RVO.
  • 最好提供一个operator+委托的非成员T::operator+=( T const & ).

我还想补充一点,完美的转发可以用来减少所需的非成员operator+重载次数:

template< typename L, typename R >
typename std::enable_if<
  std::is_convertible< L, T >::value &&
  std::is_convertible< R, T >::value,
  T >::type operator+( L && l, R && r )
{
  T result( std::forward< L >( l ) );
  result += r;
  return result;
}
Run Code Online (Sandbox Code Playgroud)

对于一些运算符来说,这个"通用"版本就足够了,但由于加法通常是可交换的,我们可能想检测右手操作数何时是右值并修改它而不是移动/复制左手操作数.这需要一个版本作为左值的右侧操作数:

template< typename L, typename R >
typename std::enable_if<
  std::is_convertible< L, T >::value &&
  std::is_convertible< R, T >::value &&
  std::is_lvalue_reference< R&& >::value,
  T >::type operator+( L && l, R && r )
{
  T result( std::forward< L >( l ) );
  result += r;
  return result;
}
Run Code Online (Sandbox Code Playgroud)

另一个是右手操作数,它们是右值:

template< typename L, typename R >
typename std::enable_if<
  std::is_convertible< L, T >::value &&
  std::is_convertible< R, T >::value &&
  std::is_rvalue_reference< R&& >::value,
  T >::type operator+( L && l, R && r )
{
  T result( std::move( r ) );
  result += l;
  return result;
}
Run Code Online (Sandbox Code Playgroud)

最后,您可能也对Boris KolpackovSumant Tambe提出的技术以及Scott Meyers 对这个想法的回应感兴趣.