简体   繁体   English

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

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

I am coding a small numeric analysis library in C++. 我正在使用C ++编写一个小型数值分析库。 I have been trying to implement using the latest C++11 features including move semantics. 我一直在尝试使用最新的C ++ 11功能实现,包括移动语义。 I understand the discussion and top answer at the following post: C++11 rvalues and move semantics confusion (return statement) , but there is one scenario that I still am trying to wrap my head around. 我理解以下帖子中的讨论和最佳答案: C ++ 11 rvalues和移动语义混淆(return语句) ,但有一种情况我仍然试图包围我的头脑。

I have a class, call it T , which is fully equipped with overloaded operators. 我有一个类,称之为T ,它配备了重载运算符。 I also have both copy and move constructors. 我也有复制和移动构造函数。

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

My client code heavily uses operators, so I am trying to ensure that complex arithmetic expressions get maximum benefit from move semantics. 我的客户端代码大量使用运算符,所以我试图确保复杂的算术表达式从移动语义中获得最大的好处。 Consider the following: 考虑以下:

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

Without move semantics, my operators are making a new local variable using the copy constructor each time, so there are a total of 4 copies. 没有移动语义,我的操作符每次都使用复制构造函数创建一个新的局部变量,所以总共有4个副本。 I was hoping that with move semantics I could reduce this to 2 copies plus some moves. 我希望通过移动语义,我可以将其减少到2个副本加上一些动作。 In the parenthesized version: 在括号中:

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

each of (b * c) and (d / e) must create the temporary in the usual way with a copy, but then it would be great if I could leverage one of those temporaries to accumulate the remaining results with only moves. (b * c)(d / e)必须以通常的方式创建临时副本,但是如果我可以利用其中一个临时数来仅用移动来累积剩余的结果,那将是很好的。

Using g++ compiler, I have been able to do this, but I suspect my technique may not be safe and I want to fully understand why. 使用g ++编译器,我已经能够做到这一点,但我怀疑我的技术可能不安全,我想完全理解为什么。

Here is an example implementation for the addition operator: 以下是加法运算符的示例实现:

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

Without the calls to std::move , then only the const & version of each operator is ever invoked. 如果没有调用std::move ,那么只调用每个运算符的const &版本。 But when using std::move as above, subsequent arithmetic (after the innermost expressions) are performed using the && version of each operator. 但是当如上所述使用std::move时,使用每个运算符的&&版本执行后续算法(在最内层表达式之后)。

I know that RVO can be inhibited, but on very computationally-expensive, real-world problems it seems that the gain slightly outweighs the lack of RVO. 我知道RVO可以被抑制,但在计算成本非常高的现实问题上,似乎收益略微超过了RVO的缺乏。 That is, over millions of computations I do get a very tiny speedup when I include std::move . 也就是说,当我包含std::move时,在数百万次计算中,我确实获得了非常小的加速。 Though in all honesty it is fast enough without. 虽然说实话,但没有足够快。 I really just want to fully comprehend the semantics here. 我真的只想完全理解这里的语义。

Is there a kind C++ Guru who is willing to take the time to explain, in a simple way, whether and why my use of std::move is a bad thing here? 是否有一位C ++ Guru愿意花时间以简单的方式解释我是否以及为何使用std :: move在这里是一件坏事? Many thanks in advance. 提前谢谢了。

You should prefer overloading the operators as free functions to obtain full type symmetry (same conversions can be applied on the left and right hand side). 您应该更喜欢将运算符重载为自由函数以获得完全类型对称(可以在左侧和右侧应用相同的转换)。 That makes it a bit more obvious what you are missing from the question. 这使得你在问题中遗漏的内容更加明显。 Restating your operator as free functions you are offering: 将您的操作员重新设置为您提供的免费功能:

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

But you are failing to provide a version that handles the left hand side being a temporary: 但是你没有提供一个处理左侧是临时的版本:

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

And to avoid ambiguities in the code when both arguments are rvalues you need to provide yet another overload: 并且当两个参数都是rvalues时,为了避免代码中出现歧义,您需要提供另一个重载:

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

The common advice would be to implement += as a member method that modifies the current object, and then write operator+ as a forwarder that modifies the appropriate object in the interface. 常见的建议是将+=实现为修改当前对象的成员方法,然后将operator+编写为转换器,以修改接口中的相应对象。

I have not really thought this much, but there might be an alternative using T (no r/lvalue reference), but I fear that it will not reduce the number of overloads you need to provide to make operator+ efficient in all circumstances. 我真的没有想过这么多,但是可能有一个使用T (没有r / lvalue引用)的替代方案,但是我担心它不会减少你需要提供的重载次数,以便在所有情况下使operator+高效。

To build on what others have said: 以其他人所说的为基础:

  • The call to std::move in T::operator+( T const & ) is unnecessary and could prevent RVO. T::operator+( T const & )调用std::move是不必要的,可以防止RVO。
  • It would be preferable to provide a non-member operator+ that delegates to T::operator+=( T const & ) . 最好提供一个非成员operator+委托给T::operator+=( T const & )

I'd also like to add that perfect forwarding can be used to reduce the number of non-member operator+ overloads required: 我还想补充一点,完美转发可用于减少所需的非成员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;
}

For some operators this "universal" version would be sufficient, but since addition is typically commutative we'd probably like to detect when the right-hand operand is an rvalue and modify it rather than moving/copying the left-hand operand. 对于一些运算符来说,这个“通用”版本就足够了,但由于加法通常是可交换的,我们可能想检测右手操作数何时是右值并修改它而不是移动/复制左手操作数。 That requires one version for right-hand operands that are lvalues: 这需要一个版本作为左值的右侧操作数:

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

And another for right-hand operands that are 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;
}

Finally, you may also be interested in a technique proposed by Boris Kolpackov and Sumant Tambe as well as Scott Meyers' response to the idea. 最后,您可能也对Boris KolpackovSumant Tambe提出的技术以及Scott Meyers对这个想法的回应感兴趣。

I agree with David Rodríguez that it'd be a better design to use non-member operator+ functions, but I'll set that aside and focus on your question. 我同意DavidRodríguez认为使用非成员operator+函数是一个更好的设计,但我会把它放在一边,专注于你的问题。

I'm surprised that you see a performance degradation when writing 写作时,你会发现性能下降,我感到很惊讶

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

instead of 代替

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

because in the former case, the compiler should be able to use RVO to construct result in the memory for the function's return value. 因为在前一种情况下,编译器应该能够使用RVO在内存中为函数的返回值构造result In the latter case, the compiler would need to move result into the function's return value, hence incur the extra cost of the move. 在后一种情况下,编译器需要将result移动到函数的返回值中,因此会产生额外的移动成本。

In general, the rules for this kind of thing are, assuming you have a function returning an object (ie, not a reference): 一般来说,假设你有一个函数返回一个对象(即,不是引用),这种事情的规则是:

  • If you're returning a local object or a by-value parameter, don't apply std::move to it. 如果要返回本地对象或按值参数,请不要将std::move应用于它。 That permits the compiler to perform RVO, which is cheaper than a copy or a move. 这允许编译器执行RVO,这比副本或移动便宜。
  • If you're returning a parameter of type rvalue reference, apply std::move to it. 如果要返回类型为rvalue的参数, std::move应用于它。 That turns the parameter into an rvalue, hence permitting the compiler to move from it. 这会将参数转换为右值,从而允许编译器从中移动。 If you just return the parameter, the compiler must perform a copy into the return value. 如果只返回参数,编译器必须执行返回值的副本。
  • If you're returning a parameter that's a universal reference (ie, a " && " parameter of deduced type that could be an rvalue reference or an lvalue reference), apply std::forward to it. 如果您返回的是一个通用引用的参数(即推导类型的“ && ”参数,可以是右值引用或左值引用),请将std::forward应用于它。 Without it, the compiler must perform a copy into the return value. 没有它,编译器必须执行返回值的副本。 With it, the compiler can perform a move if the reference is bound to an rvalue. 有了它,如果引用绑定到右值,编译器可以执行移动。

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

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