|
Boost : |
Subject: Re: [boost] [operators] The Dangling Reference Polarization
From: Daniel Frey (d.frey_at_[hidden])
Date: 2013-05-04 06:49:26
On 04.05.2013, at 08:52, Dave Abrahams <dave_at_[hidden]> wrote:
> on Thu Apr 25 2013, Daniel Frey <d.frey-AT-gmx.de> 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
}
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk