gccclangoperatorsnrvo

Symmetric operator+ in terms of operator+= in modern C++?


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.


Solution

  • 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