繁体   English   中英

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

[英]Move Semantics and Pass-by-Rvalue-Reference in Overloaded Arithmetic

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

我有一个类,称之为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在这里是一件坏事? 提前谢谢了。

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

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引用)的替代方案,但是我担心它不会减少你需要提供的重载次数,以便在所有情况下使operator+高效。

以其他人所说的为基础:

  • T::operator+( T const & )调用std::move是不必要的,可以防止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;
}

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

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对这个想法的回应感兴趣。

我同意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应用于它。 没有它,编译器必须执行返回值的副本。 有了它,如果引用绑定到右值,编译器可以执行移动。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM