1

I was reading this note on the implementation of symmetric operators in Boost.Operator https://www.boost.org/doc/libs/1_69_0/libs/utility/operators.htm#symmetry and I suspect it is awfully outdated.

The discussion revolves on what is the best way to implement operator+ generically, if a consistent operator+= is available. The conclusion there is that it is (was),

T operator+( const T& lhs, const T& rhs ){
   T nrv( lhs ); nrv += rhs; return nrv;
}

because at the time some compilers supported NRVO, as opposed to RVO.

Now, with NRVO being mandatory, and all sorts of optimization being performed, is this still the case?

For example other version that may make sense now for certain cases is:

    T operator+(T lhs, const T& rhs ){
       T ret(std::move(lhs)); ret += rhs; return ret;
    }

or

    T operator+(T lhs, const T& rhs ){
      lhs += rhs; return lhs;
    }

Given a class that has a constructor, a move constructor, and reasonable operator+=. For example:

#include<array>
#include<algorithm>

using element = double; // here double, but can be more complicated
using array = std::array<double, 9>; // here array, but can be more complicated

array& operator+=(array& a, array const& b){
    std::transform(begin(a), end(a), begin(b), begin(a), [](auto&& x, auto&& y){return x + y;});
    return a;
}
array& operator+=(array&& a, array const& b){
    std::transform(begin(a), end(a), begin(b), begin(a), [](auto&& x, auto&& y){return x + std::move(y);});
    return a;
}

What is the best way to implement a symmetric operator+? Here is a set of possible codes

/*1*/ array sum(array const& a, array const& b){array tmp(a); tmp+=b; return tmp;} // need operator+= and copy-constructor
/*2*/ array sum(array const& a, array const& b){return array(a)+=b;} // needs operator+= && and copy-constructor
/*3*/ array sum(array a, array const& b){return std::move(a)+=b;} // needs operator+= && and can use move-constructor
/*4*/ array sum(array a, array const& b){array tmp(std::move(a)); tmp+=b; return tmp;} // needs operator+= and can use move-constructor

I tried it in https://godbolt.org/z/2YPhcg and just by counting the number of assembly lines, which all other things being equal might tell what is the best implementation. I get these mixed results:

| code       | gcc -O2     | clang  -O2   |
|:-----------|------------:|:------------:|
| /*1*/      |   33 lines  |     64 lines |
| /*2*/      |   39 lines  |     59 lines |
| /*3*/      |   33 lines  |     62 lines |
| /*4*/      |   33 lines  |     64 lines |

While /*3*/ and /*4*/ can benefit from calls of the form sum(std::move(a), b) or even sum(sum(a, c), b).

So is T tmp(a); tmp+=b; return tmp; still the best way to implement operator+(T [const&], T const&)?

It looks like if there is a move constructor and a moving +=, there are other possibilities but only seem to produce simpler assembly in clang.

alfC
  • 14,261
  • 4
  • 67
  • 118

1 Answers1

1

If the signature is:

T operator+(T const& a, T const& b )

(as you say in the bolded question text), then the body should be:

return T(a) += b;

where the result object is the only T constructed. The version T nrv( lhs ); nrv += rhs; return nrv; theoretically risks the compiler not merging nrv with the result object.


Note that the above signature does not allow moving out of any of the arguments. If it's desirable to move out of the lhs, then this seems optimal to me:

T operator+(T const& a, T const& b)
{
    return T(a) += b;
}

T operator+(T&& a, T const& b)
{
    return T(std::move(a)) += b;
}

In both cases the result object is guaranteed to be the only object constructed. In the "classic" version taking T a, then the case of an rvalue argument would incur an extra move.

If you want to move out of the right-hand side then two more overloads can be added :)

Note that I have not considered the case of returning T&& for reasons described here

M.M
  • 138,810
  • 21
  • 208
  • 365
  • I don't get the relation to the linked question exactly can you lay it out? – alfC Feb 05 '19 at 04:52
  • @alfC I'm saying I don't think it would be a good idea to do `T&& operator+(...stuff...)` – M.M Feb 05 '19 at 05:04
  • Yes, I don't know I have been using lvalue references for a while and I still don't understand what does it means to return T&&. – alfC Feb 05 '19 at 06:37
  • Technically, if the operation is commutative, as `+` should be (well except for `std::string`, using `+` for std::string was a mistake), then one can implement this for all the combinations an permuntations of `T&`, `T const&`, `T&&` (9 combinations). Also there is a hidden assumption here that I realize, this is not universal, it only applies for "value types" since `T(a)` is supposed to create an independent copy. (i.e. reference-like types will not behave well.) – alfC Feb 06 '19 at 05:05
  • I edited to use `T const&` instead of `T&`; there would only be at most 4 permutations. Whether or not moving out of the r.h.s. is useful depends on the properties of the class. (E.g. for `std::string` there's nothing to be gained by moving out of both operands) – M.M Feb 06 '19 at 05:10
  • ah, yes that makes sense. – alfC Feb 06 '19 at 05:18
  • In the second case, what would be the difference with using `return std::move(a) += b;`? – alfC Feb 06 '19 at 06:33
  • @alfC that would be the same as `return a += b;`, i.e. call `a.operator+=` to update `a` in-place , but then it would copy-construct the *result object* from `a` because `operator+=` returns an lvalue reference. – M.M Feb 06 '19 at 10:36