[operators] The Dangling Reference Polarization
I see some good discussion going on, thanks to everyone participating! Rest assured that I value both side's contributions, it is important for me (and those agreeing with my perspective) to be able to defend this position and also to keep looking for improvements. If our side can not be defended, it's probably not good enough :) That said, here's what I hope can serve as a first summary, let's first look at it from a technical perspective: 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. Technically, there is no doubt that the above is a bug, but that doesn't answer the question of whether this is a bug of the user who wrote the above code or if it's a bug of the operator to return an rvalue reference instead of an rvalue. This is where we leave the technical-aspects-land and enter the minefield of opinions, expectations and decisions. I am of the opinion that the above problematic use-cases are a bug on the users side, my line-of-thoughts and my arguments are: a) The above code is explicit, you can spot it in the users code. It's not something that happens silently. b) The above code always looked suspicious to me anyways, it has a code-smell that I just don't like. c) It rely's on the fact that it extends the lifetime of a returned temporary value. The rules of the C++ standard that allow this are quite clear, and hence it's the users responsibility to make sure that there actually is a temporary value whose lifetime can be extended. It has always been an error if the expression returned a reference and not a value. Consider const std::string& oops = std::vector<std::string>(1, "hello, world" ).front(); This compiles are creates a dangling reference, as the standard says that front() returns a reference and not a value. It's not 100% comparable as in this case the standard guarantees a bug, but the main point for C++ has always been which guarantees are given and not about a certain expectation one might have. As an example: static_assert( std::is_empty< std::tuple<> >::value, "Expectation 1 broken" ); static_assert( std::is_empty< std::array< T, 0 > >::value, "Expectation 2 broken" ); // with any valid T The first case is certainly a reasonable expectation and it currently works for all implementations of std::tuple that I know of, but it is not guaranteed! If your code relies on it and it breaks in the future, you can not blame the person who modified std::tuple. The second case is also a reasonable expectation, but it breaks everywhere and AFAIK there is no standard-conforming way to implement std::array so that std::array<T,0> is empty. For the curious, here's the StackOverflow question I asked about it: <http://stackoverflow.com/q/15512827/2073257>. Another example about how to properly handle expectations vs. guarantees: <http://stackoverflow.com/q/14882588/2073257>. d) I fail to see any valid use-case for binding the result of the expression to a reference. Can someone please provide a convincing example of why (P1) or (P2) are needed/useful? Keep in mind that in the context of operator+ (or any other operator in question), we already require that the type T is copy-/moveable and that copy-elision most likely takes place if you use "T r = …" instead of "const T& r = …". e) The alternative needs to prove its claimed/theoretical efficiency in practice. Too often I have heard about "sufficiently smart compilers", but I'm more the prove-it-in-the-real-world type. So summarizing it once again: For a) and b), this is just an observation and my personal opinion. c) means it's important to talk about guarantees, not just about expectations (no matter how reasonable they are). d) poses the question if we loose something if we don't guarantee that (P1) and (P2) are allowed. I currently have several ideas/option of how we could improve Boost.Operators, some of which I already mentioned, some which only recently occurred to me. Before I will discuss these, I'd like to resolve (if possible) the Dangling Reference Polarization and gather more opinions/feedback. So far, I found the discussion both challenging and very helpful, again thanks to everyone participating! Best regards, Daniel
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.
d) I fail to see any valid use-case for binding the result of the expression to a reference. Can someone please provide a convincing example of why (P1) or (P2) are needed/useful? Keep in mind that in the context of operator+ (or any other operator in question), we already require that
VS2012 behaves differently. Not only is T &a = b + c + d not a compile error, but in all cases I've tested the temporary's life is actually extended! I'll raise questions to MS to get their input to see if they intended this, or if it's a bug (never know which it is with MS). My tests with GCC does mirror your observations. the type T is copy-/moveable and
that copy-elision most likely takes place if you use "T r = …" instead of "const T& r = …".
Found one which fails with gcc 4.7.2 (again, VS2012's trickery extends object lifetimes so it "works"): for(char c : str1 + str2 + str3) { // ... } The temporary returned is destructed prematurely.
On 25.04.2013, at 23:32, Andrew Ho <helloworld922@gmail.com> wrote:
d) I fail to see any valid use-case for binding the result of the expression to a reference. Can someone please provide a convincing example of why (P1) or (P2) are needed/useful? Keep in mind that in the context of operator+ (or any other operator in question), we already require that the type T is copy-/moveable and that copy-elision most likely takes place if you use "T r = …" instead of "const T& r = …".
Found one which fails with gcc 4.7.2 (again, VS2012's trickery extends object lifetimes so it "works"):
for(char c : str1 + str2 + str3) { // ... }
The temporary returned is destructed prematurely.
Your example suggests you tested with std::string, which works for me with GCC. But I guess you are suggesting that with an operator+ returning an rvalue reference, it would fail. And indeed this is the first example which makes sense to me. It has all the properties I was looking for: - It's common - The binding of the result is implicit - The code doesn't have a bad code-smell, it looks reasonable Mist. (That's the German "Mist", not the English one ;) Let me think about it for a while… Best regards, Daniel
Daniel Frey <d.frey <at> gmx.de> writes:
Your example suggests you tested with std::string, which works for me with GCC. But I guess you are suggesting that with an operator+ returning an rvalue reference, it would fail. And indeed this is the first example which makes sense to me. It has all the properties I was looking for:
- It's common - The binding of the result is implicit - The code doesn't have a bad code-smell, it looks reasonable
Mist. (That's the German "Mist", not the English one ;)
Let me think about it for a while…
Best regards, Daniel
Yes, I'm not using std::string but a custom string implementation. p.s. I asked a question on the msdn forums to understand why their implementation properly (or improperly, depending on your point of view) extends the lifetimes of the temporaries. link: http://social.msdn.microsoft.com/Forums/en-US/vcgeneral/thread/3c754c4e-5471... 4095-afae-795c1f411612
AMDG On 04/25/2013 01:40 PM, Daniel Frey wrote:
<snip>
c) It rely's on the fact that it extends the lifetime of a returned temporary value. The rules of the C++ standard that allow this are quite clear, and hence it's the users responsibility to make sure that there actually is a temporary value whose lifetime can be extended. It has always been an error if the expression returned a reference and not a value. Consider
const std::string& oops = std::vector<std::string>(1, "hello, world" ).front();
This compiles are creates a dangling reference, as the standard says that front() returns a reference and not a value. It's not 100% comparable as in this case the standard guarantees a bug, but the main point for C++ has always been which guarantees are given and not about a certain expectation one might have.
The designer of a component should try to make sure that the guarantees provided match user expectations. Anything that's clever, but has subtly different semantics is likely to be a source of bugs. The problem I have with returning a reference in this case, is that the result is /logically/ a value. Therefore, it's reasonable for users to expect that the result is a real value (and in fact, users could safely rely on this in C++03, because there were no rvalue references). The fact that the result is a real value some of the time, depending on the details of the arguments, makes it even worse. Also, if Boost.Operators can return an rvalue reference, then retrofitting Boost.Operators into an existing class is a breaking change, because code that was valid is no longer so.
As an example:
static_assert( std::is_empty< std::tuple<> >::value, "Expectation 1 broken" ); static_assert( std::is_empty< std::array< T, 0 > >::value, "Expectation 2 broken" ); // with any valid T
The first case is certainly a reasonable expectation and it currently works for all implementations of std::tuple that I know of, but it is not guaranteed! If your code relies on it and it breaks in the future, you can not blame the person who modified std::tuple. The second case is also a reasonable expectation, but it breaks everywhere and AFAIK there is no standard-conforming way to implement std::array so that std::array<T,0> is empty. For the curious, here's the StackOverflow question I asked about it: <http://stackoverflow.com/q/15512827/2073257>. Another example about how to properly handle expectations vs. guarantees: <http://stackoverflow.com/q/14882588/2073257>.
d) I fail to see any valid use-case for binding the result of the expression to a reference. Can someone please provide a convincing example of why (P1) or (P2) are needed/useful? Keep in mind that in the context of operator+ (or any other operator in question), we already require that the type T is copy-/moveable and that copy-elision most likely takes place if you use "T r = …" instead of "const T& r = …".
It's really not a question of whether binding a reference is actually better. It's supported by the language, and you can't detect the error at compile-time.
e) The alternative needs to prove its claimed/theoretical efficiency in practice. Too often I have heard about "sufficiently smart compilers", but I'm more the prove-it-in-the-real-world type.
Um... I think that argument goes the other way. As the person introducing an optimization, it's your responsibility to show that it actually helps. In Christ, Steven Watanabe
On 26.04.2013, at 00:01, Steven Watanabe <watanabesj@gmail.com> wrote:
On 04/25/2013 01:40 PM, Daniel Frey wrote:
<snip>
c) It rely's on the fact that it extends the lifetime of a returned temporary value. The rules of the C++ standard that allow this are quite clear, and hence it's the users responsibility to make sure that there actually is a temporary value whose lifetime can be extended. It has always been an error if the expression returned a reference and not a value. Consider
const std::string& oops = std::vector<std::string>(1, "hello, world" ).front();
This compiles are creates a dangling reference, as the standard says that front() returns a reference and not a value. It's not 100% comparable as in this case the standard guarantees a bug, but the main point for C++ has always been which guarantees are given and not about a certain expectation one might have.
The designer of a component should try to make sure that the guarantees provided match user expectations. Anything that's clever, but has subtly different semantics is likely to be a source of bugs.
I think one should try to match a users expectation and intuitions as much as possible, but only if there is no reason to diverge from it. I'm not ignoring the users reasonable expectations, I just thought that there is a good reason to consider an alternative.
e) The alternative needs to prove its claimed/theoretical efficiency in practice. Too often I have heard about "sufficiently smart compilers", but I'm more the prove-it-in-the-real-world type.
Um... I think that argument goes the other way. As the person introducing an optimization, it's your responsibility to show that it actually helps.
In my initial post from 2013-04-22T21:46 I gave an example program which shows/proves the benefit of my technique over the alternative for todays compilers. I was referring to the claimed efficiency of the alternative from Dave Abraham's article on that topic (and especially the "Reality Bites" section). But I guess most of it is mood now since Andrew Ho pointed out range-based for loops… BR, Daniel
on Thu Apr 25 2013, Daniel Frey <d.frey-AT-gmx.de> wrote:
I see some good discussion going on, thanks to everyone participating! Rest assured that I value both side's contributions, it is important for me (and those agreeing with my perspective) to be able to defend this position and also to keep looking for improvements. If our side can not be defended, it's probably not good enough :)
That said, here's what I hope can serve as a first summary, let's first look at it from a technical perspective:
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 :-) -- Dave Abrahams
On 04.05.2013, at 08:52, Dave Abrahams <dave@boostpro.com> 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 }
On 5/4/2013 5:49 AM, Daniel Frey wrote:
#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 ); } #
One note of interest. Do not replace: T operator-( T lhs, const T& rhs ) { lhs -= rhs; return lhs; } with T operator-( T lhs, const T& rhs ) { return lhs -= rhs; } At least on msvc11. PASS_BY_VALUE 0 copies, 1 moves, 0 lvalue ops, 1 rvalue ops, V r = V()-V() 0 copies, 1 moves, 1 lvalue ops, 0 rvalue ops, V r = V()-t 0 copies, 1 moves, 1 lvalue ops, 0 rvalue ops, V r = t+V() 1 copies, 0 moves, 0 lvalue ops, 1 rvalue ops, V r = t-V() 1 copies, 1 moves, 1 lvalue ops, 0 rvalue ops, V r = t-t 1 0 1 0 (expected) 1 copies, 2 moves, 2 lvalue ops, 0 rvalue ops, V r = t-t-t 1 1 2 0 (expected) 1 copies, 2 moves, 2 lvalue ops, 0 rvalue ops, V r = t+(t+t) 1 1 2 0 (expected) 2 copies, 1 moves, 1 lvalue ops, 1 rvalue ops, V r = t-(t-t) 2 0 1 1 (expected) 2 copies, 3 moves, 2 lvalue ops, 1 rvalue ops, V r = (t-t)-(t-t) 2 1 2 1 (expected) 1 copies, 3 moves, 3 lvalue ops, 0 rvalue ops, V r = t-t-t-t 1 2 3 0 (expected) 1 copies, 4 moves, 4 lvalue ops, 0 rvalue ops, V r = t-t-t-t-t 1 3 4 0 (expected) becomes PASS_BY_VALUE 0 copies, 1 moves, 0 lvalue ops, 1 rvalue ops, V r = V()-V() 1 copies, 0 moves, 1 lvalue ops, 0 rvalue ops, V r = V()-t 0 1 1 0 (expected) 0 copies, 1 moves, 1 lvalue ops, 0 rvalue ops, V r = t+V() 1 copies, 0 moves, 0 lvalue ops, 1 rvalue ops, V r = t-V() 2 copies, 0 moves, 1 lvalue ops, 0 rvalue ops, V r = t-t 1 0 1 0 (expected) 3 copies, 0 moves, 2 lvalue ops, 0 rvalue ops, V r = t-t-t 1 1 2 0 (expected) 1 copies, 2 moves, 2 lvalue ops, 0 rvalue ops, V r = t+(t+t) 1 1 2 0 (expected) 3 copies, 0 moves, 1 lvalue ops, 1 rvalue ops, V r = t-(t-t) 2 0 1 1 (expected) 4 copies, 1 moves, 2 lvalue ops, 1 rvalue ops, V r = (t-t)-(t-t) 2 1 2 1 (expected) 4 copies, 0 moves, 3 lvalue ops, 0 rvalue ops, V r = t-t-t-t 1 2 3 0 (expected) 5 copies, 0 moves, 4 lvalue ops, 0 rvalue ops, V r = t-t-t-t-t 1 3 4 0 (expected)
On 04.05.2013, at 19:12, Michael Marcin <mike.marcin@gmail.com> wrote:
On 5/4/2013 5:49 AM, Daniel Frey wrote:
#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 ); } #
One note of interest.
Do not replace: T operator-( T lhs, const T& rhs ) { lhs -= rhs; return lhs; } with T operator-( T lhs, const T& rhs ) { return lhs -= rhs; }
At least on msvc11.
Thanks for testing, the results for MSVC are similar to the results I got with GCC and Clang and are also what I expected, as the result from lhs-=rhs is T& and hence can only lead to a copy instead of a move. If anything, you could try "return std::move( lhs -= rhs );" but from my tests and what I got from Dave's article about pass-by-value, "lhs-=rhs; return lhs;" is the "correct" version which might allow future optimizations.
On 5/4/2013 1:53 PM, Daniel Frey wrote:
On 04.05.2013, at 19:12, Michael Marcin <mike.marcin@gmail.com> wrote:
On 5/4/2013 5:49 AM, Daniel Frey wrote:
#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 ); } #
One note of interest.
Do not replace: T operator-( T lhs, const T& rhs ) { lhs -= rhs; return lhs; } with T operator-( T lhs, const T& rhs ) { return lhs -= rhs; }
At least on msvc11.
Thanks for testing, the results for MSVC are similar to the results I got with GCC and Clang and are also what I expected, as the result from lhs-=rhs is T& and hence can only lead to a copy instead of a move. If anything, you could try "return std::move( lhs -= rhs );" but from my tests and what I got from Dave's article about pass-by-value, "lhs-=rhs; return lhs;" is the "correct" version which might allow future optimizations.
Here is another data point: http://ideone.com/GgaZZV And here are msvc11 results: http://codepad.org/XFT5qVyE based on: http://stackoverflow.com/questions/1693005/copy-elision-on-visual-c-2010-bet...
On 04.05.2013, at 21:45, Michael Marcin <mike.marcin@gmail.com> wrote:
Here is another data point: http://ideone.com/GgaZZV
And here are msvc11 results: http://codepad.org/XFT5qVyE
based on: http://stackoverflow.com/questions/1693005/copy-elision-on-visual-c-2010-bet...
From what I can see these are some more basic tests and they confirm that copy elision does not take place for pass-by-value (for MSVC, but my tests also confirmed that for GCC/Clang), i.e., the last test case "identity( rvalue() )" does not eliminate the temporary. Is there anything else that can be learned from the above that I missed? BTW: Careful when testing the (N)RVO on earlier versions of MSVC/GCC, I seem to remember that some compilers where allergic to any code before the named variable that was returned. In code: X foo() { X x; scope_logger sl(); return x; } // compiler performed the NRVO X foo() { scope_logger sl(); X x; return x; } // no NRVO Although this does not seem to be a problem for MSVC11 or any recent version of GCC.
On 5/4/2013 5:36 PM, Daniel Frey wrote:
On 04.05.2013, at 21:45, Michael Marcin <mike.marcin@gmail.com> wrote:
Here is another data point: http://ideone.com/GgaZZV
And here are msvc11 results: http://codepad.org/XFT5qVyE
based on: http://stackoverflow.com/questions/1693005/copy-elision-on-visual-c-2010-bet...
From what I can see these are some more basic tests and they confirm that copy elision does not take place for pass-by-value (for MSVC, but my tests also confirmed that for GCC/Clang), i.e., the last test case "identity( rvalue() )" does not eliminate the temporary. Is there anything else that can be learned from the above that I missed?
MSVC11 does do copy elision when only trivial types are involved it seems. For example try instead of std::vector try: typedef std::array<std::pair<int,int>,4> pod; Direct initialization from rvalue, constructed with pod X a(( rvalue(pod()) )) =========== X5: construct (with pod) X5: destroy ----------- 0/1 possible copies made 0/1 possible moves made 1/1 possible elisions performed However any more complicated it seems to give up. To my mind the following should be a trivial change that has no impact. Indeed gcc-4.7.2 has no change but msvc11: Direct initialization from rvalue, constructed with pod X a(( true, rvalue(pod()) )) =========== X3: construct (with pod) X4: <- X3: **move** X3: destroy X4: destroy ----------- 0/1 possible copies made 1/1 possible moves made 0/1 possible elisions performed
On 05.05.2013, at 01:22, Michael Marcin <mike.marcin@gmail.com> wrote:
MSVC11 does do copy elision when only trivial types are involved it seems. For example try instead of std::vector try: typedef std::array<std::pair<int,int>,4> pod;
Direct initialization from rvalue, constructed with pod X a(( rvalue(pod()) )) =========== X5: construct (with pod) X5: destroy ----------- 0/1 possible copies made 0/1 possible moves made 1/1 possible elisions performed
I am not concerned about direct initialization, the important case (for the discussion of Boost.Operators' future) is the "identity( rvalue() )"-case. Of course MSVC (like other compilers) does perform copy elision in some cases, but I am still looking for a case like identity(), where a parameter is pass-by-value and that parameter is returned from the function. Is there some way that the copy/move of the parameter can be avoided? In the test-program I wrote, is there a way to beat STANDARD with PASS_BY_VALUE?
participants (5)
-
Andrew Ho
-
Daniel Frey
-
Dave Abrahams
-
Michael Marcin
-
Steven Watanabe