Reputation: 60481
Consider the following code that build a class storing functions.
// Function list class
template <class... F>
struct function_list
{
template <class... G>
constexpr function_list(G&&... g) noexcept
: _f{std::forward<G>(g)...}
{
}
std::tuple</* F... OR F&&... */> _f;
};
// Function list maker
template <class... F, class R = /* Can we compute the return type here? */>
constexpr R make_function_list(F&&... f)
{
return function_list<
/* decltype(std::forward<F>(f))...
* OR F...
* OR F&&...
*/>(std::forward<F>(f)...);
}
I would like these functions to be perfectly forwarded (regardless of whether they are function pointers, functors, lambdas...). But I don't exactly understand all the type deduction happening behind std::forward
and universal references. In the code above, I have three questions:
_f
be of type std::tuple<F...>
or std::tuple<F&&...>
(and why?)R
in the template parameter list (because doing it manually instead of auto/decltype(auto)
would be helpful to understand what is going on)function_list
template argument should be: decltype(std::forward<F>(f)...)
, F
, or F&&...
(and why?)Note: the constructor of function_list
is not meant to be called directly, instead make_function_list
is doing the job.
EDIT:
Is this case safe, when the operator()
of function_list
(not shown here) is not guaranted to be called on the same statement?
template <class... F>
constexpr function_list<F...> make_function_list(F&&... f)
{
return function_list<F&&...>(std::forward<F>(f)...);
}
Upvotes: 3
Views: 548
Reputation: 13589
It depends on what function_list
is for. There basically are two cases:
function_list
is a temporary helper that should never outlive the statement it appears in. Here we can store references to functions and perfect-forward each of them to the point of invocation:
template <class... F>
struct function_list
{
std::tuple<F&&...> f_;
// no need to make this template
constexpr function_list(F&&... f) noexcept
: f_{std::forward<F>(f)...}
{}
template <std::size_t i, typename... A>
decltype(auto) call_at(A&&... a)
{
return std::invoke(std::get<i>(f_), std::forward<A>(a)...);
}
};
function_list
is a wrapper/container object akin to std::bind
, in this case you'd want to store decayed copies of the functions to avoid dangling references and perfect-forwarding in this context would mean forwarding functions to the constructors of their decayed versions in f_
and then at the point of call imbuing the decayed functions with value category of the function_list
itself:
template <class... F>
struct function_list
{
std::tuple<std::decay_t<F>...> f_;
template <typename... G>
constexpr function_list(G&&... g)
: f_{std::forward<G>(g)...}
{}
template <std::size_t i, typename... A>
decltype(auto) call_at(A&&... a) &
{
return std::invoke(std::get<i>(f_), std::forward<A>(a)...);
}
template <std::size_t i, typename... A>
decltype(auto) call_at(A&&... a) const&
{
return std::invoke(std::get<i>(f_), std::forward<A>(a)...);
}
template <std::size_t i, typename... A>
decltype(auto) call_at(A&&... a) &&
{
return std::invoke(std::get<i>(std::move(f_)), std::forward<A>(a)...);
}
template <std::size_t i, typename... A>
decltype(auto) call_at(A&&... a) const&&
{
return std::invoke(std::get<i>(std::move(f_)), std::forward<A>(a)...);
}
};
As with std::bind
if you actually want to store a reference, you must do so explicitly with std::reference_wrapper
.
Construction is the same in both cases:
template <class... F>
constexpr auto make_function_list(F&&... f)
{
return function_list<F...>(std::forward<F>(f)...);
}
Upvotes: 0
Reputation: 93384
But I don't exactly understand all the type deduction happening behind
std::forward
and universal references.
It's quite simple to understand via an example.
template <typename T>
void f(T&&)
{
std::tuple<T>{}; // (0)
std::tuple<T&&>{}; // (1)
}
In the case of (0):
T
is deduced as T
for rvaluesT
is deduced as T&
for lvalues.In the case of (1):
T
is deduced as T&&
for rvaluesT
is deduced as T&
for lvalues.As you can see, the only difference between two is how rvalues are deduced.
Regarding std::forward
, this is what it does:
template <typename T>
void g(T&&);
template <typename T>
void f(T&& x)
{
g(x) // (0)
g(std::forward<T>(x)); // (1)
}
In the case of (0):
x
is always an lvalue.In the case of (1):
x
is casted to T&&
if T
is deduced as T
.
x
stays an lvalue otherwise.
std::forward
basically retains the type category of x
by looking at how T
was deduced.
Should _f be of type
std::tuple<F...>
orstd::tuple<F&&...>
I think that in your case it should be std::tuple<F...>
, as you want to store either lvalue references or values.
std::tuple<F&&...>
would store either lvalue references or rvalue references - that would lead to dangling references in the case of temporaries.
Is it possible to deduce the return type
R
in the template parameter list
Yes, it is just function_list<F...>
.
template <class... F, class R = function_list<F...>>
constexpr R make_function_list(F&&... f)
{
return function_list<F...>(std::forward<F>(f)...);
}
You don't even need the R
template parameter.
template <class... F>
constexpr function_list<F...> make_function_list(F&&... f)
{
return function_list<F...>(std::forward<F>(f)...);
}
In the maker, what the
function_list
template argument should be:decltype(std::forward<F>(f)...)
,F
, orF&&...
function_list
should take F...
as a template parameter for the reasons listed at the beginning of this answer (i.e. avoiding dangling references to temporaries).
It should still take std::forward<F>(f)...
as its arguments to allow rvalues to be forwarded as such (i.e. moving rvalues into function_list
's tuple).
Upvotes: 3
Reputation: 275896
If they are F&&
, then if you pass a temporary to make_function_list
, the returned class containing a tuple
will store an rvalue reference to the temporary passed to make_function_list
.
On the next line, it is now a dangling reference.
This seems bad in most use cases. This is not actually bad in all use cases; forward_as_tuple
does this. But such use cases are not general use cases. The pattern is extremely brittle and dangerous.
In general, if you are returning a T&&
, you want to return it as a T
. This can cause a copy of the object; but the alternative is danging-reference-hell.
This gives us:
template<class... Fs>
struct function_list {
template<class... Gs>
explicit constexpr function_list(Gs&&... gs) noexcept
: fs(std::forward<Gs>(gs)...)
{}
std::tuple<Fs...> fs;
};
template<class... Fs, class R = function_list<Fs...>>
constexpr R make_function_list(Fs&&... fs) {
return R(std::forward<Fs>(fs)...);
}
Also make function_list
's ctor explicit
, because in the 1 argument case it devolves to a rather greedy implicit conversion constructor. This can be fixed but takes more effort than it is worth.
operator()
requires an instance. A type name is not an instance.
Upvotes: 2