首页 文章

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

提问于
浏览
12

我正在用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大师愿意花时间以简单的方式解释我是否以及为何使用std :: move在这里是一件坏事?提前谢谢了 .

3 回答

  • 3

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

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

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

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

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

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

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

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

  • 8

    以其他人所说的为基础:

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

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

    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的response提出的技术感兴趣 .

  • 5

    我同意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 应用 std::move . 这允许编译器执行RVO,这比副本或移动便宜 .

    • 如果要返回rvalue类型参数,请对其应用 std::move . 这会将参数转换为右值,从而允许编译器从中移动 . 如果只返回参数,编译器必须执行返回值的副本 .

    • 如果您是通用引用(即推导类型的“ && ”参数,可以是右值引用或左值引用),请对其应用 std::forward . 没有它,编译器必须执行返回值的副本 . 有了它,如果引用绑定到右值,编译器可以执行移动 .

相关问题