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.