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

时间:2012-10-31 19:21:46

标签: c++ c++11 rvalue-reference move-semantics

我正在使用C ++编写一个小型数值分析库。我一直在尝试使用最新的C ++ 11功能实现,包括移动语义。我理解以下帖子中的讨论和最佳答案:C++11 rvalues and move semantics confusion (return statement),但有一种情况我仍在试图解决这个问题。

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

T (const T &) { /*initialization via copy*/; }
T (T &&) { /*initialization via move*/; }

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

T a, b, c, d, e;
T f = a + b * c - d / e;

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

T f = a + (b * c) - (d / e);

(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);
}

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

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

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

3 个答案:

答案 0 :(得分:8)

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

T operator+( T const &, T const & );
T operator+( T const &, T&& );

但是你没有提供一个处理左手边是暂时的版本:

T operator+( T&&, T const& );

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

T operator+( T&&, T&& );

通常的建议是将+=实现为修改当前对象的成员方法,然后将operator+编写为修改界面中相应对象的转发器。

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

答案 1 :(得分:5)

以他人所说的为基础:

  • std::move中对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;
}

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

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;
}

另一个右手操作数是rvalues:

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;
}

最后,您可能也会对Boris KolpackovSumant Tambe以及Scott Meyers'response提出的技巧感兴趣。

答案 2 :(得分:3)

我同意DavidRodríguez认为使用非成员operator+函数更好的设计,但我会把它放在一边,专注于你的问题。

我很惊讶您在编写

时看到性能下降
T operator+(const T&)
{
  T result(*this);
  return result;
}

而不是

T operator+(const T&)
{
  T result(*this);
  return std::move(result);
}

因为在前一种情况下,编译器应该能够使用RVO在内存中构造result函数的返回值。在后一种情况下,编译器需要将result移动到函数的返回值中,因此会产生额外的移动成本。

一般来说,假设你有一个函数返回一个对象(即不是引用),这种事情的规则是:

  • 如果您要返回本地对象或按值参数,请不要对其应用std::move。这允许编译器执行RVO,这比复制或移动便宜。
  • 如果您要返回rvalue类型参数,请对其应用std::move。这会将参数转换为右值,从而允许编译器从中移动。如果只返回参数,编译器必须执行返回值的复制。
  • 如果您要返回的参数是通用引用(即推导类型的“&&”参数,可以是右值引用或左值引用),请对其应用std::forward。没有它,编译器必须执行返回值的副本。有了它,如果引用绑定到右值,编译器就可以执行移动。