Search code examples
c++c++11tuplesforwarding

Simulating std::forward with std::forward_as_tuple


Let's say I use std::forward_as_tuple to store the arguments of a function call in a tuple

auto args = std::forward_as_tuple(std::forward<Args>(args)...);

And then I pass this tuple by lvalue reference to a function that wants to invoke a function foo() with some of the arguments in args as determined by another std::integer_sequence. I do it with std::move() like so

template <typename TupleArgs, std::size_t... Indices>
decltype(auto) forward_to_foo(TupleArgs&& args, 
                              std::index_sequence<Indices...>) {
    return foo(std::get<Indices>(std::move(args))...);
}

And this would work because the rvalue qualified version of std::get<std::tuple> return std::tuple_element_t<Index, tuple<Types...>>&& which is an identity transformation of the reference-ness of the std::tuple_element_t<Index, tuple<Types...>> because of reference collapsing with the &&.

So if std::tuple_element_t<Index, tuple<Types...>> evaluates to T& the returned type would be T& && which is just T&. Similar reason for when std::tuple_element_t<Index, tuple<Types...>> returns T&& and T

Am I missing something? Are there some cases where this would fail?


Solution

  • template <typename TupleArgs, std::size_t... Indices>
    decltype(auto) forward_to_foo(TupleArgs&& args, 
                              std::index_sequence<Indices...>) {
      return foo(std::get<Indices>(std::forward<TupleArgs>(args))...);
    }
    

    This is the correct implementation.

    Use should look like:

    auto tuple_args = std::forward_as_tuple(std::forward<Args>(args)...);
    forward_to_foo( std::move(tuple_args), std::make_index_sequence<sizeof...(args)>{} );
    

    there are a few differences here.

    First, we take by forwarding reference, not by lvalue reference. This lets the caller provide rvalue (prvalue or xvalue) tuples to us.

    Second, we forward the tuple into the std::get call. This means we only pass get an rvalue reference if the tuple was moved into us.

    Third, we move into forward_to_foo. This ensures the above does the right thing.

    Now, imagine if we wanted to call foo twice.

    auto tuple_args = std::forward_as_tuple(std::forward<Args>(args)...);
    auto indexes = std::make_index_sequence<sizeof...(args)>{};
    forward_to_foo( tuple_args, indexes );
    forward_to_foo( std::move(tuple_args), indexes );
    

    we don't have to touch forward_to_foo at all, and we never move from any of the args more than once.

    With your original implementation, any calls to forward_to_foo silently move from TupleArgs rvalue references or values without any indication at the call-site that we are destructive on the first parameter.

    Other than that detail, yes that emulates forwarding.


    Myself I'd just write a notstd::apply:

    namespace notstd {
      namespace details {
        template <class F, class TupleArgs, std::size_t... Indices>
        decltype(auto) apply(F&& f, TupleArgs&& args, 
                          std::index_sequence<Indices...>) {
          return std::forward<F>(f)(std::get<Indices>(std::forward<TupleArgs>(args))...);
        }
      }
      template <class F, class TupleArgs>
      decltype(auto) apply(F&& f, TupleArgs&& args) {
        constexpr auto count = std::tuple_size< std::decay_t<TupleArgs> >{};
        return details::apply(
          std::forward<F>(f),
          std::forward<TupleArgs>(args),
          std::make_index_sequence< count >{}
        );
      }
    }
    

    then we do:

    auto tuple_args = std::forward_as_tuple(std::forward<Args>(args)...);
    auto call_foo = [](auto&&...args)->decltype(auto){ return foo(decltype(args)(args)...); };
    return notstd::apply( call_foo, std::move(tuple_args) );
    

    which moves the tricky bit into notstd::apply, which attempts to match the semantics of std::apply, which lets you replace it down the road with a more standard bit of code.