Perfect forwarding with a temporary function wrapper - c++

Consider the following code in C++14, following the posts here, here and here:
// Include
#include <tuple>
#include <iostream>
#include <type_traits>
// Temporary function queue declaration
template <class... F>
class temporary_function_queue;
// Apply function queue declaration
template <class... F>
constexpr temporary_function_queue<F&&...> apply_function_queue(F&&... f);
// Temporary function queue definition
template <class... F>
class temporary_function_queue final
{
// Types
private:
using const_lvalue_reference = const temporary_function_queue&;
using rvalue_reference = temporary_function_queue&&;
using temporary_type = temporary_function_queue<F&&...>;
using data_type = std::tuple<F&&...>;
// Lifecycle
private:
temporary_function_queue(rvalue_reference) = default;
temporary_function_queue(const_lvalue_reference) = delete;
temporary_function_queue operator=(rvalue_reference) = delete;
temporary_function_queue operator=(const_lvalue_reference) = delete;
explicit constexpr temporary_function_queue(F&&... f)
: _f{std::forward<F>(f)...}
{
}
// Temporary creator
public:
friend constexpr temporary_type apply_function_queue<>(F&&... f);
// Apply function queue declaration
public:
template <class... Args>
decltype(auto) operator()(Args&&... args) const&&
{
// Do I need to do std::forward on f0 too? If so, how?
return std::get<0>(_f)(std::forward<Args>(args)...);
}
// Data members
private:
data_type _f;
};
// Apply function queue definition
template <class... F>
constexpr temporary_function_queue<F&&...> apply_function_queue(F&&... f)
{
return temporary_function_queue<F&&...>(std::forward<F>(f)...);
}
/* Example of use
int main(int argc, char* argv[])
{
apply_function_queue(
[](auto i){std::cout<<0<<std::endl;},
[](auto i){std::cout<<1<<std::endl;}
)(0);
return 0;
}
*/
The goal is to produce the following call syntax:
apply_function_queue(f0, f1, f2)(a, b, c, d, e);
Where f0, f1, f2 are either function pointers, functors, lambdas..., and where a, b, c, d, e are arguments to be perfectly forwarded. This function should produce a temporary type, and then call the operator() of that temporary type, and this operator should do a perfect forwarding of fn (for now f0, it will be changed later) with the arguments a, b, c, d, e.... The temporary_function_queue should not be usable in any other context.
The problem is that I am a little lost with the forwarding, universal references and lvalue references... Is the code shown above safe? If not what example of use would lead to undefined behaviour? And in that case, how to make it safe, and efficient (ideally, I would like no runtime overhead on most compilers with -O3)?

Note: Dangling references are quite likely to happen with this approach.
auto make_f(); // return by value
auto&& q = apply_function_queue(make_f());
// q holds dangling rvalue reference
q(a, b, c); // whoops...
First some remarks about wording and deduction. Let:
template<class... T> void f(T&&... p) {}
Note: When this template is instantiated, there are two different packs here: T... and T&&....
Call it with an lvalue or type R and an rvalue of type Q:
R a;
f(a, Q{});
Now T... will be R&, Q but T&&... will be R&, Q&&.
Forwarding the pack p will result in the T&&... pack.
'decltype'(std::forward<T>(p)...) === T&&...
(Note: You can't actually apply decltype here - it's just for illustration.)
Therefore, I'll call the pack that is actually deduced (T...) the deduced types/pack and the result of adding rvalue references / forwarding (T&&...) the forwarded types/pack.
Applying && everywhere inside the class as well as in the return type of apply_function_queue is superfluous. (If you return temporary_function_queue<F&&...> from apply_function_queue there is no need for && inside temporary_function_queue. And if you apply && inside the class everywhere there is no need to return temporary_function_queue<F&&...>.)
You either instantiate the class template with the deduced pack and add && everywhere you want references our you instantiate the class template with the forwarding pack and don't add &&.
It is required to have the deduced types available in the class. (Because the friend declaration uses both F... and F&&....) So you'll want to remove && from the return type of apply_function_queue.
You'll need to change some declarations:
apply_function_queue
forward declaration:
template <class... F>
constexpr temporary_function_queue<F...> apply_function_queue(F&&... f);
definition:
template <class... F>
constexpr temporary_function_queue<F...> apply_function_queue(F&&... f)
{
return temporary_function_queue<F...>(std::forward<F>(f)...);
}
temporary_type
The class instance type is temporary_function_queue<F...> not temporary_function_queue<F&&...>!
using temporary_type = temporary_function_queue<F...>;
friend declaration
friend constexpr temporary_type apply_function_queue<F...>(F&&... f);
If you want perfect forwarding of rvalue function types in the function call operator you'll have to resort to manual casting / forwarding I think.
Inside decltype(auto) operator()(Args&&... args) const&& you'll find that
decltype(std::get<0>(_f)) === std::tuple_element_t<0u, data_type>&
which by reference collapsing rules is a lvalue reference. What you actually want, in order to forward the elements from the tuple is the tuple_element::type.
Thus you'd have to directly cast to the actual type in the tuple:
return static_cast<std::tuple_element_t<0u, data_type>>(
std::get<0>(_f))(std::forward<Args>(args)...);
or forward (which will have the same effect through reference collapsing):
return std::forward<std::tuple_element_t<0u, data_type>>(
std::get<0>(_f))(std::forward<Args>(args)...);

Related

Can function templates be used as first class citizens in higher order function calls?

Passing a function template as argument to another function template is always a bit tricky. Typically one has to resort to creating a lambda object that goes and calls the original function.
Example
template <typename It>
void f(It, It) {}
void g(std::vector<int>::iterator, std::vector<int>::iterator) {}
template <typename C, typename F>
void callA(C&& c, F callable) {
return callable(std::begin(c), std::end(c));
}
Problem
If I have a std::vector<int> c, I cannot just pass f along to callA because f is a template and not a function:
callA(c, f); // f has an unresolved overloaded function type and cannot be deduced
callA(c, std::distance); // same problem
callA(c, g); // works because g is a proper function, not a template
callA(c, [](auto a, auto b) {return f(a,b);}); // works again
Even if we help deduce the type of the callable:
template <typename C, template <typename> typename F,
typename T = std::decay_t<decltype(std::begin(std::declval<C>()))>>
auto callB(C&& c, F<T> callable) {
return callable(std::begin(c), std::end(c));
}
this fails to compile.
Question
Is there a way to force C++ to deduce the function's type directly without resorting to a lambda function or type-erasure like std::function? Effectively, (how) can I turn a function template into a first-class citizen? That is, have its type deduced.
I'm willing to go to some lengths when defining the higher-order function (see callB versus callA), but not when calling it.
You can change the parameter type to function pointer, which could be converted from function template (with template argument deduced). E.g.
template <typename C>
void callA(C&& c, void(*callable) (std::decay_t<decltype(std::begin(std::declval<C>()))>, std::decay_t<decltype(std::begin(std::declval<C>()))>)) {
return callable(std::begin(c), std::end(c));
}
Then
std::vector<int> c;
callA(c, f); // template argument It will be deduced as std::vector<int>::iterator for f
LIVE
Are you aware of BOOST_HOF_LIFT?
It allows you to lift overloaded and/or templated functions into objects with as easy syntax as auto my_max = BOOST_HOF_LIFT(std::max);.
You could change callA(c, std::distance) to callA(c, BOOST_HOF_LIFT(std::distance)).

Meaning of std::forward<std::decay_t<F>>(f)

I have a question about the code written in https://koturn.hatenablog.com/entry/2018/06/10/060000
When I pass a left value reference, if I do not remove the reference with std::decay_t, I get an error.
Here is the error message
'error: 'operator()' is not a member of 'main()::<lambda(auto:11, int)>&
I don't understand why it is necessary to exclude the left value reference.
I would like to know what this error means.
#include <iostream>
#include <utility>
template <typename F>
class
FixPoint : private F
{
public:
explicit constexpr FixPoint(F&& f) noexcept
: F{std::forward<F>(f)}
{}
template <typename... Args>
constexpr decltype(auto)
operator()(Args&&... args) const
{
return F::operator()(*this, std::forward<Args>(args)...);
}
}; // class FixPoint
namespace
{
template <typename F>
inline constexpr decltype(auto)
makeFixPoint(F&& f) noexcept
{
return FixPoint<std::decay_t<F>>{std::forward<std::decay_t<F>>(f)};
}
} // namespace
int
main()
{
auto body = [](auto f, int n) -> int {
return n < 2 ? n : (f(n - 1) + f(n - 2));
};
auto result = makeFixPoint(body)(10);
std::cout << result << std::endl;
}
I don't understand why it is necessary to exclude the left value
reference. I would like to know what this error means.
When you pass an lvalue lambda into makeFixPoint(), the template parameter F of makeFixPoint() is instantiated as L&, where L is the lambda type. In the function body, FixPoint<std::decay_t<F>>{...} will be instantiated as FixPoint<L>{...}, so the constructor of FixPoint is instantiated as
explicit constexpr FixPoint(L&& f);
which accepts a lambda type of rvalue reference. In makeFixPoint(), if you initialize it with {std::forward<F>(f)} i.e. {std::forward<L&>(f)}, f will be forwarded as an lvalue, which will be ill-formed since rvalue reference cannot bind an lvalue.
The purpose of using {std::forward<std::decay_t<F>>(f)} is to force f to be forwarded as an rvalue.
,The code you have shared is toxic.
template <typename F>
class FixPoint : private F
{
public:
explicit constexpr FixPoint(F&& f)
what this means is that we expect F to be a value type (because inheriting from a reference isn't possible). In addition, we will only accept rvalues -- we will only move from another F.
: F{std::forward<F>(f)}
{}
this std::forward<F> is pointless; this indicates that the person writing this code thinks they are perfect forwarding: they are not. The only legal types F are value types (not references), and if F is a value type F&& is always an rvalue reference, and thus std::forward<F> is always equivalent to std::move.
There are cases where you want to
template<class X>
struct wrap1 {
X&& x;
wrap1(X&& xin):x(std::forward<X>(xin)){}
};
or even
template<class X>
struct wrap2 {
X x;
wrap2(X&& xin):x(std::forward<X>(xin)){}
};
so the above code is similar to some use cases, but it isn't one of those use cases. (The difference here is that X or X&& is the type of a member, not a base class; base classes cannot be references).
The use for wrap2 is when you want to "lifetime extend" rvalues, and simply take references to lvalues. The use for wrap1 is when you want to continue the perfect forwarding of some expression (wrap1 style objects are generally unsafe to keep around for more than a single line of code; wrap2 are safe so long as they don't outlive any lvalue passed to them).
template <typename F>
inline constexpr decltype(auto)
makeFixPoint(F&& f) noexcept
{
return FixPoint<std::decay_t<F>>{std::forward<std::decay_t<F>>(f)};
}
Ok more red flags here. inline constexpr is a sign of nonsense; constexpr functions are always inline. There could be some compilers who treat the extra inline as meaning something, so not a guarantee of a problem.
return FixPoint<std::decay_t<F>>{std::forward<std::decay_t<F>>(f)};
std::forward is a conditional move. It moves if there is an rvalue or value type passed to it. decay is guaranteed to return a value type. So we just threw out the conditional part of the operation, and made it into a raw move.
return FixPoint<std::decay_t<F>>{std::move(f)};
this is a simpler one that has the same meaning.
At this point you'll probably see the danger:
template <typename F>
constexpr decltype(auto)
makeFixPoint(F&& f) noexcept
{
return FixPoint<std::decay_t<F>>{std::move(f)};
}
we are unconditionally moving from the makeFixPoint argument. There is nothing about the name makeFixPoint that says "I move from the argument", and it accept forwarding references; so it will silently consume an lvalue and move-from it.
This is very rude.
So a sensible version of the code is:
template <typename F>
class FixPoint : private F
{
public:
template<class U,
class dU = std::decay_t<U>,
std::enable_if_t<!std::is_same_v<dU, FixPoint> && std::is_same_v<dU, F>, bool> = true
>
explicit constexpr FixPoint(U&& u) noexcept
: F{std::forward<U>(u)}
{}
[SNIP]
namespace
{
template <typename F>
constexpr FixPoint<std::decay_t<F>>
makeFixPoint(F&& f) noexcept
{
return FixPoint<std::decay_t<F>>{std::forward<F>(f)};
}
} // namespace
that cleans things up a bit.
The template just can't work as intended this way.
First, because it is supposed to be possible to inherit from F, it doesn't make much sense to use std::decay_t. Instead it is probably supposed to be just std::remove_cvref_t, but before C++20 that was kind of cumbersome to write and std::decay_t does almost the same.
In the class template F is intended to be a non-reference type. It is not a template parameter of the constructor. So it cannot be used for perfect-forwarding. The constructor argument will always be a rvalue reference which cannot take lvalues.
Even if a lvalue is passed to makeFixPoint, the call std::forward<std::decay_t<F>>(f) forces conversion to a rvalue reference, which then can be used in the constructor. That is clearly dangerous. The function always behaves as if you had std::moved the argument into it.
It should just be std::forward<F>(f) and the constructor should take another template parameter that can be used to form a forwarding reference:
template<typename G>
requires std::is_same_v<std::remove_cvref_t<G>, F>
explicit constexpr FixPoint(G&& g) noexcept
: F{std::forward<G>(g)}
{}

C++: static assert that functor's argument is const reference

I'm writing a template function in C++17 which accepts a functor F as argument and I want to restrict the passed in functor to have only one constant reference argument, where T can be any type.
for example:
template <class T> struct my_struct{
std::vector<T> collection;
template <class F> std::vector<T> func(F f){
static_assert(
// CONDITION HERE!,
"f's argument is not const reference"
);
std::vector<T> ret;
std::copy_if(
std::make_move_iterator(this->collection.begin()),
std::make_move_iterator(this->collection.end()),
std::inserter(ret, ret.end()),
f
);
return ret;
}
};
Obviously, in case f is [](auto v){return true;} the resulting vector returned from func will have empty elements (because those are moved before adding to resulting container). So, I need to restrict possible input functors to [](const auto& v){}.
I have tried something like this:
static_assert(
std::is_invocable_v<F, const T&>,
"f's argument is not const reference"
);
But then func([](auto v){}) does not trigger the assertion, because T is copyable in my case.
The func([](auto& v){}) also passes the test, because auto can be const T.
But I need to limit possible lambdas to be func([](const auto& v){}).
You might write traits (with its limitations), something like:
template <typename Sig> struct callable_traits;
template <typename Ret, typename ...Args>
struct callable_traits<Ret(*)(Args...)>
{
using args = std::tuple<Args...>;
};
// add specialization for C-ellipsis too
template <typename Ret, class C, typename ...Args>
struct callable_traits<Ret(C::*)(Args...) const>
{
using args = std::tuple<Args...>;
};
// add specialization for variant with C-ellipsis, cv-qualifier, ref-qualifier
template <class C>
struct callable_traits<C> : callable_traits<&C::operator()>{};
Limitation of the traits: doesn't handle templated operator() (as for generic lambda), overloaded operator().
And then
template <class T> struct my_struct{
template <class F> void func(F f){
static_assert(
std::is_same_v<std::tuple<const T&>, typename callable_traits<F>::args>,
"f's argument is not const reference"
);
// here goes some code which can possibly call f with rvalue
// reference argument, so I want to avoid situation when the
// argument object is moved or modified. I don't have control
// over this code because it an STL algorithm.
}
};
I could be misunderstanding what you're trying to do but, as I read it, you want to accept a callable, then pass some argument to it with a guarantee that the argument cannot be changed (you don't want someone to accept the argument as a non-const l-value reference or an r-value reference. If so, then std::is_invocable should be enough:
#include <type_traits> // for std::is_invocable
#include <functional> // for std::invoke
template <class parameter_t> struct my_struct {
template <typename callable_t>
void function(callable_t const &callable) requires (
std::is_invocable<callable_t, parameter_t const &>::value
) {
// . . .
std::invoke(callable, parameter_t{});
// . . .
}
};
Then:
int main() {
my_struct<int>{}.function([](int const&){}); // Fine
my_struct<int>{}.function([](int){}); // Fine, a copy of the parameter is made when the lambda is invoked.
my_struct<int>{}.function([](int &){}); // error: no matching member function for call to 'function'
my_struct<int>{}.function([](int &&){}); // error: no matching member function for call to 'function'
}
(You can play around with it here)
A possible problem is that this method does allow a copy to be made, but if the main goal is to protect the variable you hold from changes this should be good enough.
P. S. I know I used c++20 requires clause as a way of future-proofing the answer, but it should be trivial to convert it to if constexpr, static_assert or any other way you prefer.
I finally managed to achieve it in the following way:
template <class T> struct my_struct{
std::vector<T> collection;
struct noncopyable_value_type : T{
noncopyable_value_type(const noncopyable_value_type&) = delete;
noncopyable_value_type& operator=(const noncopyable_value_type&) = delete;
};
template <class F> std::vector<T> func(F f){
static_assert(
std::is_invocable_v<F, noncopyable_value_type>,
"f's argument must be const reference"
);
std::vector<T> ret;
std::copy_if(
std::make_move_iterator(this->collection.begin()),
std::make_move_iterator(this->collection.end()),
std::inserter(ret, ret.end()),
f
);
return ret;
}
};
But still, the problem here is that it only works with generic lambdas.

How to convert a constructor to a std::function [duplicate]

This question already has answers here:
How to pass a function pointer that points to constructor?
(8 answers)
Construct std::function with a constructor based on template
(1 answer)
Closed 3 years ago.
Is there any way to assign a constructor to a function like 'f':
struct C
{
C(int){};
};
auto f = C::C(int); // not work
auto g = [](int){ return C(int()); }; // works
Neither C, C::C, &C, &C::C, C::&C, C::C(int), &C::C(int) ... works
Solution proposed by Friday Pie:
template <class T>
struct ConstructHelper
{
template <class... Args>
static T&& construct(Args&&... args)
{
return std::move(T(std::forward<Args>(args)...));
}
};
auto f = ConstructHelper<C>::construct<int>; // works
auto c = apply(f, 0); // works
The code you tried to write follows other rules than than the lambda that you wrote.
In the first one, you are trying to get a pointer to a member function: &C::C This would be similar to &C::print (assuming argument int) The first argument of this function would be a pointer to C, the second would the the int.
However, we are trying to do this for the constructor. So we don't have a valid C to work on. The result is that this simply doesn't compile.
Note that if you do want to execute a constructor on existing memory, you need placement new.
Your second code is a lambda. In short: a class with operator() implemented. And in this operator, you write code as in any other function and return a newly constructed instance by value. (Although, this looks a lot like a vexing parse) Tis is similar to executing the print function that was mentioned before.
So both would have different semantics and your compiler is right in failing to compile this code.
In theory your hypothetical function apply would look quite simple as a template
#include <iostream>
#include <type_traits>
struct C
{
C(int){};
};
template <class F, class... Args>
auto apply ( F&& f, Args&&... args) -> typename std::result_of<F(Args...)>::type
{
return f(args...);
}
float foo (int a, float b) { return a*b; }
int main()
{
auto b = apply(&foo, 3, 5.f);
//auto b = apply(&C::C, 3); //error: taking address of constructor 'C::C'
return 0;
}
But it is impossible to take address of constructor, as per the ISO standard. In most cases this special function is a non-entity in resulting code.
Neither explicit call to constructor is allowed. Your lambda function does thing differently. It constructs an object of class C as to per defined effect of that expression.
You could use a bogus argument and SFINAE
template <class F, class... Args>
auto apply ( F&& f, Args&&... args)
-> std::enable_if_t<std::is_invocable<F, Args...>::value,
typename std::result_of<F(Args...)>::type>
{
return f(args...);
}
template <class T, class... Args>
auto apply ( T&&, Args&&... args)
-> std::enable_if_t<!std::is_invocable<T, Args...>::value,T&&>
{
return std::move(T(args...));
}
but it's preferable to avoid that and rethink whatever meta-programming pattern you have in mind, i.e. avoid attempt to call same template name for different kinds of arguments, or make amends with lambda use. E.g. you may avoid to call apply() by using SFINAE.

Perfect forwarding of functions to build a function list class

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:
Should _f be of type std::tuple<F...> or std::tuple<F&&...> (and why?)
Is it possible to deduce the return type R in the template parameter list (because doing it manually instead of auto/decltype(auto) would be helpful to understand what is going on)
In the maker, what the 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)...);
}
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 rvalues
T is deduced as T& for lvalues.
In the case of (1):
T is deduced as T&& for rvalues
T 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...> or std::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, or F&&...
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).
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.
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)...);
}