Reputation: 5649
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.
Upvotes: 5
Views: 949
Reputation: 303497
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.
Upvotes: 6
Reputation: 873
If the operator is called with a rvalue reference as first argument, e.g. when using std::move or the direct result of a function call, the move constructor of lhs will be called. There is no need for an extra overload which takes rvalue references.
Upvotes: 0