Move semantics and operator overloading

后端 未结 2 939
一生所求
一生所求 2021-01-31 12:52

This is related to this answer provided by Matthieu M. on how to utilize move semantics with the + operator overloading (in general, operators which don\'t re-assign directly ba

相关标签:
2条回答
  • Important update / warning about this answer!

    There actually is a convincing example which silently creates a dangling reference in reasonable real-world code with the below. Please use the other answer's technique to avoid this problem even at the cost of some additional temporaries being created. I'll leave the rest of this answer untouched for future reference.


    The correct overloads for a commutative case are:

    T   operator+( const T& lhs, const T& rhs )
    {
      T nrv( lhs );
      nrv += rhs;
      return nrv;
    }
    
    T&& operator+( T&& lhs, const T& rhs )
    {
      lhs += rhs;
      return std::move( lhs );
    }
    
    T&& operator+( const T& lhs, T&& rhs )
    {
      rhs += lhs;
      return std::move( rhs );
    }
    
    T&& operator+( T&& lhs, T&& rhs )
    {
      lhs += std::move( rhs );
      return std::move( lhs );
    }
    

    Why is that and how does it work? First, notice that if you take an rvalue reference as a parameter, you can modify and return it. The expression where it comes from needs to guarantee that the rvalue won't be destructed before the end of the complete expression, including operator+. This also means that operator+ can simply return the rvalue reference as the caller needs to use the result of operator+ (which is part of the same expression) before the expression is completely evaluated and the temporaries (ravlues) are destructed.

    The second important observation is, how this saves even more temporaries and move operations. Consider the following expression:

    T a, b, c, d; // initialized somehow...
    
    T r = a + b + c + d;
    

    with the above, it is equivalent to:

    T t( a );    // T operator+( const T& lhs, const T& rhs );
    t += b;      // ...part of the above...
    t += c;      // T&& operator+( T&& lhs, const T& rhs );
    t += d;      // T&& operator+( T&& lhs, const T& rhs );
    T r( std::move( t ) ); // T&& was returned from the last operator+
    

    compare this to what happens with the other approach:

    T t1( a );   // T operator+( T lhs, const T& rhs );
    t1 += b;     // ...part of the above...
    T t2( std::move( t1 ) ); // t1 is an rvalue, so it is moved
    t2 += c;
    T t3( std::move( t2 ) );
    t3 += d;
    T r( std::move( t3 );
    

    which means you still have three temporaries and although they are moved instead of copied, the approach above is much more efficient in avoiding temporaries altogether.

    For a complete library, including support for noexcept, see df.operators. There you will also find versions for non-commutative cases and operations on mixed types.


    Here's a complete test program to test it:

    #include <iostream>
    #include <utility>
    
    struct A
    {
      A() { std::cout << "A::A()" << std::endl; }
      A( const A& ) { std::cout << "A::A(const A&)" << std::endl; }
      A( A&& ) { std::cout << "A::A(A&&)" << std::endl; }
      ~A() { std::cout << "A::~A()" << std::endl; }
    
      A& operator+=( const A& ) { std::cout << "+=" << std::endl; return *this; }
    };
    
    // #define BY_VALUE
    #ifdef BY_VALUE
    A operator+( A lhs, const A& rhs )
    {
      lhs += rhs;
      return lhs;
    }
    #else
    A operator+( const A& lhs, const A& rhs )
    {
      A nrv( lhs );
      nrv += rhs;
      return nrv;
    }
    
    A&& operator+( A&& lhs, const A& rhs )
    {
      lhs += rhs;
      return std::move( lhs );
    }
    #endif
    
    int main()
    {
      A a, b, c, d;
      A r = a + b + c + d;
    }
    
    0 讨论(0)
  • 2021-01-31 13:17

    I don't think you're missing anything -- the code in your question is indeed trouble. The earlier part of his answer made sense, but something was lost between the "four desired cases" and the actual example.

    This could be better:

    inline T operator+(T left, T const& right) { left += right; return left; }
    inline T operator+(const T& left, T&& right) { right += left; return right; }
    

    This implements the rule: Make a copy of the LHS (preferably via move construction), unless the RHS is expiring anyway, in which case modify it in place.

    For non-commutative operators, omit the second overload, or else provide an implementation that doesn't delegate to compound assignment.

    If your class has heavyweight resources embedded inside (so that it can't be efficiently moved), you'll want to avoid pass-by-value. Daniel makes some good points in his answer. But do NOT return T&& as he suggests, since that is a dangling reference.

    0 讨论(0)
提交回复
热议问题