In questions+answers like Operator Overloading, it is stated that the best way to overload a binary operator such as operator+
is:
class X {
X& operator+=(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator+(X lhs, const X& rhs)
{
lhs += rhs;
return lhs;
}
operator+
itself thus takes lhs
by value, rhs
by const reference and returns the altered lhs
by value.
I am having trouble understanding what would happen here if it would be called with an rvalue as lhs
: Would this still be the single definition that is needed (and will the compiler optimize the movement of the argument and the return value), or does it make sense to add a second overloaded version of the operator that works with rvalue references?
EDIT:
Interestingly, in Boost.Operators, they talk about this implementation:
T operator+( const T& lhs, const T& rhs )
{
T nrv( lhs );
nrv += rhs;
return nrv;
}
which allows Named Return Value Optimization but it is not used by default because:
Sadly, not all compiler implement the NRVO, some even implement it in an incorrect way which makes it useless here
This new information is not enough for me to provide a full answer, but it might allow some other bright minds to derive at an encompassing conclusion.
This signature:
inline X operator+(X lhs, const X& rhs)
allows both rvalues and lvalues as the left-hand side of the operation. lvalues would just be copied into lhs
, xvalues would be moved into lhs
, and prvalues would be initialized directly into lhs
.
The difference between taking lhs
by value and taking lhs
by const&
materializes when we chain multiple +
operations. Let's just make a table:
+====================+==============+=================+
| | X const& lhs | X lhs |
+--------------------+--------------+-----------------+
| X sum = a+b; | 1 copy | 1 copy, 1 move |
| X sum = X{}+b; | 1 copy | 1 move |
| X sum = a+b+c; | 2 copies | 1 copy, 2 moves |
| X sum = X{}+b+c; | 2 copies | 2 moves |
| X sum = a+b+c+d; | 3 copies | 1 copy, 3 moves |
| X sum = X{}+b+c+d; | 3 copies | 3 moves |
+====================+==============+=================+
Taking the first argument by const&
scales in the number of copies. Each operation is one copy. Taking the first argument by value scales in the number of copies. Each operation is just one move but for the first argument being a lvalue means an additional copy (or an additional move for xvalues).
If your type isn't cheap to move - for those cases where moving and copying are equivalent - then you want to take the first argument by const&
since it is at least as good as the other case and there's no reason to fuss.
But if it's cheaper to move, you actually probably want both overloads:
X operator+(X const& lhs, X const& rhs) {
X tmp(lhs);
tmp += rhs;
return tmp;
}
X operator+(X&& lhs, X const& rhs) {
lhs += rhs;
return std::move(lhs);
}
This will use moves instead of copies for all the intermediate temporary objects, but will save you one move on the first one. Unfortunately, the best solution is also the most verbose.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With