Search code examples
c++c++11c++14rvalue-referencestdtuple

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?


Solution

  • 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.