Vincent
Vincent

Reputation: 60481

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:

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

Answers (3)

yuri kilochek
yuri kilochek

Reputation: 13589

It depends on what function_list is for. There basically are two cases:

  1. 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)...);
        }
    };
    
  2. 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

Vittorio Romeo
Vittorio Romeo

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 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).

Upvotes: 3

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

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

Related Questions