On 04.05.2013, at 08:52, Dave Abrahams
on Thu Apr 25 2013, Daniel Frey
wrote: If an operator returns an rvalue reference (T&&), it is safe for all use-cases except binding the result to an rvalue- or const-lvalue-reference:
T r = a + b + c; // no problem const T r = a + b + c; // no problem
T& r = a + b + c; // compile-time error
const T& r = a + b + c; // problem (P1) T&& r = a + b + c; // problem (P2)
It seems that this is consistent behavior across at least GCC, Clang and VC++. If a compiler shows a different behavior, its likely a compiler-bug.
I believe Howard and I basically came to the same conclusion about returning rvalue references: Just Don't Do It. Any potential speedups are vastly outweighed by the safety costs. Return by value; it will move anyway :-)
I agree with return-by-value now, but the example that I needed to see the importance was the range-based for statement which implicitly binds a reference. I think that in some cases return-by-rvalue-reference might still be useful, but the user needs to explicitly opt-in on it and the decision should be made on a per-class case, not just by a #define. Anyways, the remaining question is whether or not pass-by-value on the parameter side could improve some cases. I attached my current test code at the end of this message. Some observations so far: 1) return-by-value and return-by-rvalue-reference are identical except for the return type, its the same set of overloads and the same implementation. 2) All three techniques differ only in the number of move-operations (and hence in the number of temporaries created). Copies and the number of called operator=-overloads (lvalue and rvalue) is the same. 3) I can't find a single case where today's compilers can benefit from pass-by-value. I'd be really interested if you could come up with an example expression where pass-by-value can lead to better code. Best regards, Daniel #include <iostream> #include <utility> unsigned copies, moves, lvalue, rvalue; void reset() { copies = moves = lvalue = rvalue = 0; } void print( const char* name ) { std::cout << copies << " copies, " << moves << " moves, " << lvalue << " lvalue ops, " << rvalue << " rvalue ops, " << "T r = " << name << std::endl; } void check( const unsigned e1, const unsigned e2, const unsigned e3, const unsigned e4 ) { if( copies != e1 || moves != e2 || lvalue != e3 || rvalue != e4 ) { std::cout << e1 << " " << e2 << " " << e3 << " " << e4 << " " << "(expected)" << std::endl; } } struct T { T() {} T( const T& ) { ++copies; } T( T&& ) { ++moves; } T& operator+=( const T& ) { ++lvalue; return *this; } T& operator+=( T&& ) { ++rvalue; return *this; } T& operator-=( const T& ) { ++lvalue; return *this; } T& operator-=( T&& ) { ++rvalue; return *this; } }; #define STANDARD // #define RETURN_RVALUE_REFERENCE // #define PASS_BY_VALUE // operator+ is commutative #ifdef STANDARD T operator+( const T& lhs, const T& rhs ) { T nrv( lhs ); nrv += rhs; return nrv; } T operator+( T&& lhs, const T& rhs ) { lhs += rhs; return std::move( lhs ); } T operator+( const T& lhs, T&& rhs ) { rhs += std::move( lhs ); return std::move( rhs ); } T operator+( T&& lhs, T&& rhs ) { lhs += std::move( rhs ); return std::move( lhs ); } #endif #ifdef RETURN_RVALUE_REFERENCE T operator+( const T& lhs, const T& rhs ) { T nrv( lhs ); nrv += rhs; return nrv; } T&& operator+( T&& lhs, const T& rhs ) { lhs += rhs; return std::move( lhs ); } T&& operator+( const T& lhs, T&& rhs ) { rhs += std::move( lhs ); return std::move( rhs ); } T&& operator+( T&& lhs, T&& rhs ) { lhs += std::move( rhs ); return std::move( lhs ); } #endif #ifdef PASS_BY_VALUE T operator+( T lhs, const T& rhs ) { lhs += rhs; return lhs; } T operator+( const T& lhs, T&& rhs ) { rhs += std::move( lhs ); return std::move( rhs ); } T operator+( T&& lhs, T&& rhs ) { lhs += std::move( rhs ); return std::move( lhs ); } #endif // operator- is non-commutative #ifdef STANDARD T operator-( const T& lhs, const T& rhs ) { T nrv( lhs ); nrv -= rhs; return nrv; } T operator-( T&& lhs, const T& rhs ) { lhs -= rhs; return std::move( lhs ); } T operator-( const T& lhs, T&& rhs ) { T nrv( lhs ); nrv -= std::move( rhs ); return nrv; } T operator-( T&& lhs, T&& rhs ) { lhs -= std::move( rhs ); return std::move( lhs ); } #endif #ifdef RETURN_RVALUE_REFERENCE T operator-( const T& lhs, const T& rhs ) { T nrv( lhs ); nrv -= rhs; return nrv; } T&& operator-( T&& lhs, const T& rhs ) { lhs -= rhs; return std::move( lhs ); } T operator-( const T& lhs, T&& rhs ) { T nrv( lhs ); nrv -= std::move( rhs ); return nrv; } T&& operator-( T&& lhs, T&& rhs ) { lhs -= std::move( rhs ); return std::move( lhs ); } #endif #ifdef PASS_BY_VALUE T operator-( T lhs, const T& rhs ) { lhs -= rhs; return lhs; } T operator-( const T& lhs, T&& rhs ) { T nrv( lhs ); nrv -= std::move( rhs ); return nrv; } T operator-( T&& lhs, T&& rhs ) { lhs -= std::move( rhs ); return std::move( lhs ); } #endif #define TEST( e1, e2, e3, e4, X ) do { reset(); T r = X; (void)r; print( #X ); check( e1, e2, e3, e4 ); } while( false ) int main() { T t; // expected talues refer to STANDARD // RETURN_RTALUE_REFERENCE optimizes some cases // PASS_BY_TALUE pessimized some cases TEST( 0, 1, 0, 1, T() - T() ); // 1 TEST( 0, 1, 1, 0, T() - t ); // 2 TEST( 0, 1, 1, 0, t + T() ); // 3.1 TEST( 1, 0, 0, 1, t - T() ); // 3.2 TEST( 1, 0, 1, 0, t - t ); // 4 TEST( 1, 1, 2, 0, t - t - t ); // 4 + 2 TEST( 1, 1, 2, 0, t + ( t + t ) ); // 4 + 3.1 TEST( 2, 0, 1, 1, t - ( t - t ) ); // 4 + 3.2 TEST( 2, 1, 2, 1, ( t - t ) - ( t - t ) ); // 4 + 4 + 1 TEST( 1, 2, 3, 0, t - t - t - t ); // 4 + 2 + 2 TEST( 1, 3, 4, 0, t - t - t - t - t ); // 4 + 2 + 2 + 2 }