5

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.

Community
  • 1
  • 1
Qqwy
  • 5,214
  • 5
  • 42
  • 83
  • 1
    Good question. In principle, the copy into lhs when calling the function can be elided, but I wonder how this plays with return value optimization. Anyway, if X has a cheap move constructor, you'll be fine with this variant. – Alexandre C. Feb 04 '17 at 10:12
  • Your implementation kills NRVO. – juanchopanza Feb 04 '17 at 10:15
  • It would help if you showed actual code corresponding to what you're describing in the last paragraph – M.M Feb 04 '17 at 10:18
  • By the way, "*it is stated that the best way to overload a binary*" in the link you mentioned is debatable. I tried changing that, and left some comments. Basically, you optimize for rvalues, and lose return value optimization. OTOH remember that at the end of the day the "as-if" rule gives the compiler a lot of latitude for optimization beyond copy elision. – juanchopanza Feb 04 '17 at 10:20
  • _A second overloaded operator_, so to say, like `X operator+(X&& lhs, const X& rhs)`? In which case, returning `lhs` would still disable NRVO. – Dean Seo Feb 04 '17 at 10:21
  • Ah, I even [wrote a few words on that](https://juanchopanzacpp.wordpress.com/2014/05/11/want-speed-dont-always-pass-by-value/) a while back. Although I have to stress again that the "asi-if" rule is very powerful. The linked article talks only about copy elision, which has a more restrictive set of rules. Apologies for linking my own blog post. – juanchopanza Feb 04 '17 at 10:30
  • 1
    For posterity, as I did not know the abbreviation: NRVO stands for _Named Return Value Optimization_ – Qqwy Feb 04 '17 at 10:44
  • See also http://stackoverflow.com/questions/9444485/why-is-rvo-disallowed-when-returning-a-parameter – juanchopanza Feb 04 '17 at 10:45
  • @juanchopanza Could you tell us what kind of second overloaded version would be applicable to not kill NRVO if it is allowed? – Qqwy Feb 04 '17 at 19:39
  • You'd need a few for the best of all worlds: `T operator+( const T& lhs, const T& rhs )`, `T operator+(T&& lhs, const T& rhs )`, `T operator+( const T& lhs, T&& rhs )`, `T operator+(T&& lhs, T&& rhs )`. – juanchopanza Feb 05 '17 at 09:19
  • @DeanSeo Right. The idea would be to `std::move(lhs)` into a local object that gets returned. – juanchopanza Feb 05 '17 at 09:20
  • @juanchopanza How would you implement the second version here? (without assuming that the operator in question is commutative). If you'd write this out as an answer, this might provide a full answer to my question that I could accept :-). – Qqwy Feb 06 '17 at 13:13

2 Answers2

6

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • There is also the option to return `X&&` to remove intermediate `move`. (with the drawback of dangling reference for `X&& res = X{} + a;`). – Jarod42 Feb 11 '17 at 13:55
0

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.

The Techel
  • 918
  • 8
  • 13