在超载算术中移动语义和传递引用

Move Semantics and Pass-by-Rvalue-Reference in Overloaded Arithmetic

本文关键字:引用 语义 移动 超载      更新时间:2023-10-16

我正在编码C 中的小数字分析库。我一直在尝试使用最新的C 11功能来实现,包括移动语义。我了解以下文章的讨论和最佳答案:C 11 rvalues并移动语义混乱(返回语句),但是我仍然试图缠绕我的头。

我有一个类,称其为T,该 CC_1完全配备了过载的操作员。我也有复制和移动构造函数。

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 上的大师愿意花时间以一种简单的方式解释我对std ::移动的使用是否以及为什么在这里是一件坏事?非常感谢。边)。这使得您从问题中缺少的东西更为明显。将操作员重申为您提供的免费功能:

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。
  • 最好提供委派给T::operator+=( T const & )的非会员operator+

我还想补充说,可以使用完美的转发来减少所需的非会员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;
}

对于某些运营商而言,此"通用"版本就足够了,但是由于添加通常是交换性的,因此我们可能想检测右侧操作数何时是rvalue并进行修改,而不是移动/复制左手操作。。这需要一个用于右手操作数的版本:

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 Kolpackov和Sumant Tambe提出的一种技术以及Scott Meyers对这个想法的回应感兴趣。

我同意戴维·罗德里格斯(DavidRodríguez

我很惊讶您在写作时会看到性能退化

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,该RVO比副本便宜或移动。
  • 如果您要返回类型RVALUE参考的参数,请将std::move应用于它。这将参数变成了一个rvalue,因此允许编译器从中移动。如果您只是返回参数,则编译器必须执行副本到返回值中。
  • 如果您要返回一个通用引用的参数(即推论类型的" &&"参数,该参数可能是RVALUE参考或LVALUE参考),请将std::forward应用于它。没有它,编译器必须在返回值中执行副本。使用它,如果引用绑定到rvalue,则可以执行编译器。