Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Move semantics and operator overloading

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 back to the left param).

He suggested implementing three distinct overloads:

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

Number 1 and 3 make sense, but I don't understand what purpose 2 does. The comment suggests commutative handling, but it seems that 1 and 2 would be mutually exclusive (i.e. implementing both results in ambiguities)

For example, with all 3 implemented:

T a, b, c;
c = a + b;

Compiler output:

1>          error C2593: 'operator +' is ambiguous
1>          could be 'T operator +(const T &,T)'
1>          or       'T operator +(T,const T &)'
1>          while trying to match the argument list '(T, T)'

removing either 1 or 2 and the program works as expected. Since 1 is the general case and 2 only works correctly with a commutative operator, I don't see why 2 would ever be used. Is there something I'm missing?

like image 466
helloworld922 Avatar asked Apr 21 '13 21:04

helloworld922


2 Answers

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.

like image 180
Ben Voigt Avatar answered Sep 30 '22 07:09

Ben Voigt


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;
}
like image 36
Daniel Frey Avatar answered Sep 30 '22 05:09

Daniel Frey