Subject: [boost] [operators] The Dangling Reference Polarization
From: Daniel Frey (d.frey_at_[hidden])
Date: 2013-04-25 16:40:50
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