Semantics moving and operator overloading

This is due to this answer provided by Matthieu M. on how to use move semantics with operator + overloading (in general, assign operators directly back to the left parameter).

He proposed implementing three different 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 

The numbers 1 and 3 make sense, but I don’t understand what goal 2 does. The comment suggests commutative processing, but it seems that 1 and 2 will be mutually exclusive (i.e., implement both results in an ambiguity)

For example, when doing all 3:

 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)'

removal of 1 or 2 and the program is working properly. Since 1 is a general case, and 2 works correctly with a commutative operator, I don’t understand why 2 will ever be used. Is something missing?

+8
c ++ c ++ 11 move-semantics operator-overloading move
source share
2 answers

I don’t think you are missing something - the code in your question is really a problem. The earlier part of his answer made sense, but something was lost between the “four desired cases” and the actual example.

It 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 using the move construct) if the RHS still does not expire, in which case change it in place.

For non-commutative statements, omit the second overload or provide an implementation that does not delegate complex assignment.

If your class has heavy resources embedded inside (so that it cannot be moved efficiently), you should avoid passing by value. Daniel gives good answers in his answer. But DO NOT return T&& as it suggests, as this is a chatty link.

+11
source share

Important update / warning about this answer!

Actually there is a convincing example which silently creates a freezing link in reasonable real code with the below. Please use another's response technique to avoid this problem, even at the cost of creating additional time periods. I will leave the rest of this answer untouched for future reference.


The correct overloads for the 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 it and how does it work? First, note that if you take the rvalue reference as a parameter, you can change it and return it. The expression in which it proceeds must ensure that the rvalue value is not destroyed until the end of the full expression, including operator+ . This also means that operator+ can simply return a rvalue reference, since the caller needs to use the result of operator+ (which is part of the same expression) before the expression is fully evaluated and the temporary (ravlues) are destroyed.

A second important point is how it saves even more temporary and moving operations. Consider the following expression:

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

with the above, this 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 that you still have three temporary files, and although they are moved rather than copied, the approach given above is much more efficient in avoiding temporary situations.

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 is the full test program for testing:

 #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; } 
+5
source share

All Articles