Reputation: 5387
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
Reputation: 48467
My understanding is that in the
std::tuple<Args&&...>
theArgs
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");
}
Upvotes: 4