The following code:
#include <tuple>
int main ()
{
auto f = [] () -> decltype (auto)
{
return std::get<0> (std::make_tuple (0));
};
return f ();
}
(Silently) generates code with undefined behaviour - the temporary rvalue returned by make_tuple
is propagated through the std::get<> and through the decltype(auto) onto the return type. So it ends up returning a reference to a temporary that has gone out of scope. See it here https://godbolt.org/g/X1UhSw.
Now, you could argue that my use of decltype(auto)
is at fault. But in my generic code (where the type of the tuple might be std::tuple<Foo &>
) I don't want to always make a copy. I really do want to extract the exact value or reference from the tuple.
My feeling is that this overload of std::get
is dangerous:
template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >&&
get( tuple<Types...>&& t ) noexcept;
Whilst propagating lvalue references onto tuple elements is probably sensible, I don't think that holds for rvalue references.
I'm sure the standards committee thought this through very carefully, but can anyone explain to me why this was considered the best option?
In the example, the main function passes an rvalue to f . The body of f treats its named parameter as an lvalue. The call from f to g binds the parameter to an lvalue reference (the first overloaded version of g ). You can cast an lvalue to an rvalue reference.
rvalue references have two properties that are useful: rvalue references extend the lifespan of the temporary object to which they are assigned. Non-const rvalue references allow you to modify the rvalue.
An rvalue reference is formed by placing an && after some type. An rvalue reference behaves just like an lvalue reference except that it can bind to a temporary (an rvalue), whereas you can not bind a (non const) lvalue reference to an rvalue.
But where exactly is this rvalue stored? It's up to the compiler where to store a temporary; the standard only specifies its lifetime. Typically, it will be treated like an automatic variable, stored in registers or in the function's stack frame.
Consider the following example:
void consume(foo&&);
template <typename Tuple>
void consume_tuple_first(Tuple&& t)
{
consume(std::get<0>(std::forward<Tuple>(t)));
}
int main()
{
consume_tuple_first(std::tuple{foo{}});
}
In this case, we know that std::tuple{foo{}}
is a temporary and that it will live for the entire duration of the consume_tuple_first(std::tuple{foo{}})
expression.
We want to avoid any unnecessary copy and move, but still propagate the temporarity of foo{}
to consume
.
The only way of doing that is by having std::get
return an rvalue reference when invoked with a temporary std::tuple
instance.
live example on wandbox
Changing std::get<0>(std::forward<Tuple>(t))
to std::get<0>(t)
produces a compilation error (as expected) (on wandbox).
Having a get
alternative that returns by value results in an additional unnecessary move:
template <typename Tuple>
auto myget(Tuple&& t)
{
return std::get<0>(std::forward<Tuple>(t));
}
template <typename Tuple>
void consume_tuple_first(Tuple&& t)
{
consume(myget(std::forward<Tuple>(t)));
}
live example on wandbox
but can anyone explain to me why this was considered the best option?
Because it enables optional generic code that seamlessly propagates temporaries rvalue references when accessing tuples. The alternative of returning by value might result in unnecessary move operations.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With