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?
The correct overloads for a commutative case are:
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 thatoperator+
can simply return the rvalue reference as the caller needs to use the result ofoperator+
(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:
with the above, it is equivalent to:
compare this to what happens with the other approach:
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:
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:
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.