Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is the behaviour of std::get on rvalue-reference tuples dangerous?

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?

like image 235
Matt A Avatar asked Dec 20 '17 11:12

Matt A


People also ask

Can you pass an lvalue to an rvalue reference?

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.

What is the use of 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.

Is an rvalue reference an 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.

Where is rvalue stored?

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.


1 Answers

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.

like image 91
Vittorio Romeo Avatar answered Oct 16 '22 21:10

Vittorio Romeo