Search code examples
c++c++11rvalue-referencemove-semantics

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


I am coding a small numeric analysis library in C++. I have been trying to implement using the latest C++11 features including move semantics. 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.

I have a class, call it T, which is fully equipped with overloaded operators. 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. I was hoping that with move semantics I could reduce this to 2 copies plus some moves. 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.

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.

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. But when using std::move as above, subsequent arithmetic (after the innermost expressions) are performed using the && version of each operator.

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. That is, over millions of computations I do get a very tiny speedup when I include 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? Many thanks in advance.


Solution

  • 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:

    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.

    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.