SU3
SU3

Reputation: 5387

Wrap a pack of forwarding references in a tuple

I have a function like this

template <typename... Args> void foo(Args&&... args);

to which I need to add an extra parameter at the end with a default argument. Since the pack needs to come last, I'm thinking of changing the function to

template <typename... Args> void foo(std::tuple<Args&&...> args,
                                     const std::string& name = {});

The question is, what is the best way to pass the arguments in a tuple. My understanding is that in the std::tuple<Args&&...> the Args are not forwarding references anymore, but strictly rvalue references. How do I get the forwarding references behavior for args wrapped in a tuple, e.g. accept an std::forward_as_tuple and preserve the reference types of the individual tuple elements. Also, what's the best way to pass the tuple here,

std::tuple<Args&&...> args

or

const std::tuple<Args&&...>& args

or

std::tuple<Args&&...>&& args

?

And do I need to use std::forward on the tuple elements inside the function, or simply std::get them?

Upvotes: 3

Views: 993

Answers (1)

Piotr Skotnicki
Piotr Skotnicki

Reputation: 48467

My understanding is that in the std::tuple<Args&&...> the Args are not forwarding references anymore

Correct.

but strictly rvalue references

Yes, unless Args are specified explicitly, in which case reference collapsing can turn them into lvalue references, i.e., foo<int&>(...) will result in Args&& -> int& && -> int&.

what is the best way to pass the arguments in a tuple.

That depends on the intended usage of foo. If you don't need to know what Args... exactly are, you can probably get away with:

template <typename Tuple>
void foo(Tuple&& args, const std::string& name = {});

In such a case, individual types are still accessible using std::tuple_element_t<N, std::decay_t<Tuple>>.

If you do want to know Args... inside foo (without any additional levels of abstraction), you probably want to deduce the exact types, without any referenceness:

template <typename.... Args>
void foo(std::tuple<Args...>&& args, const std::string& name = {});

Note that if someone uses std::forward_as_tuple with lvalues and rvalues inside, the value categories will be stored in Args and you can still forward those arguments using std::forward (std::forward is not limited to forwarding references only, think of it as a conditional cast).

Also, what's the best way to pass the tuple here

Probably Tuple&& as suggested earlier. If not, then again it depends on the usage. If you use const std::tuple<Args...>&, then by looking at the list of overloads for std::get, you'll see that the the value category and constness propagates to the return value of std::get (modulo reference collapsing). The same is with std::tuple<Args...>&&. Also, using the latter, you will have to use a tuple rvalue as an argument (foo(std::forward_as_tuple(...), ...) as opposed to foo(my_tuple, ...)).

An alternative solution would be to accept a parameter pack, and detect whether the last parameter is something that can be bound by const std::string& or not:

#include <string>
#include <utility>
#include <tuple>
#include <type_traits>

struct dummy {};

template <typename... Args>
void foo_impl(Args&&... args)
{
    const std::string& s = std::get<sizeof...(Args) - 1>(std::forward_as_tuple(std::forward<Args>(args)...));
}

template <typename... Args>
auto foo(Args&&... args)
    -> std::enable_if_t<std::is_constructible<std::string, std::tuple_element_t<sizeof...(Args), std::tuple<dummy, Args...>>>{}>
{
    foo_impl(std::forward<Args>(args)...);
}

template <typename... Args>
auto foo(Args&&... args)
    -> std::enable_if_t<!std::is_constructible<std::string, std::tuple_element_t<sizeof...(Args), std::tuple<dummy, Args...>>>{}>
{
    foo_impl(std::forward<Args>(args)..., "default");
}

DEMO

Upvotes: 4

Related Questions