功能完美转发构建功能列表class

Perfect forwarding of functions to build a function list class

考虑以下构建 class 存储函数的代码。

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

我希望这些函数能完美转发(不管是函数指针、函子、lambdas...)。但我并不完全理解 std::forward 和通用引用背后发生的所有类型推导。在上面的代码中,我有三个问题:

注意:function_list 的构造函数不是直接调用的,而是 make_function_list 正在做的。

编辑: 如果不保证在同一语句中调用 function_listoperator()(此处未显示),这种情况是否安全?

template <class... F>
constexpr function_list<F...> make_function_list(F&&... f)
{
    return function_list<F&&...>(std::forward<F>(f)...);
}

如果它们是 F&&,那么如果您将临时值传递给 make_function_list,包含 tuple 的 returned class 将存储一个右值对传递给 make_function_list.

的临时引用

在下一行,它现在是悬空引用。

这在大多数用例中似乎很糟糕。这在 all 用例中实际上并不坏; forward_as_tuple 这样做。但此类用例 不是 一般用例。该模式非常脆弱和危险。

一般来说,如果您 return 正在 T&&,您希望 return 将其作为 T。这可能会导致对象的副本;但替代方案是 danging-reference-hell.

这给了我们:

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

也使 function_list 的 ctor explicit,因为在 1 个参数的情况下,它转变成一个相当贪婪的隐式转换构造函数。这可以修复,但需要花费更多的精力。

operator() 需要实例。类型名称不是实例。

But I don't exactly understand all the type deduction happening behind std::forward and universal references.

举个例子就很容易理解了。

template <typename T>
void f(T&&)
{
    std::tuple<T>{}; // (0)
    std::tuple<T&&>{}; // (1)
}

(0)的情况下:

  • T 推导为 T for rvalues
  • T 推导为 T& for lvalues.

(1):

  • T 推导为 T&& for rvalues
  • T 推导为 T& for lvalues.

如您所见,两者之间的唯一区别在于 rvalues 的推导方式。

关于std::forward,这是它的作用:

template <typename T>
void g(T&&);

template <typename T>
void f(T&& x)
{
    g(x) // (0)
    g(std::forward<T>(x)); // (1)
}

(0)的情况下:

  • x 始终是一个 左值

(1):

  • x 转换为 T&& 如果 T 推导为 T.

  • x 保持 lvalue 否则。

std::forward 通过查看 T 的推导方式,基本上保留了 x 的类型类别。


Should _f be of type std::tuple<F...> or std::tuple<F&&...>

我认为在你的情况下它应该是 std::tuple<F...>,因为你想存储 左值引用 .

std::tuple<F&&...> 将存储 左值引用 右值引用 - 这将导致 悬空引用 在临时的情况下。


Is it possible to deduce the return type R in the template parameter list

是的,就是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)...);
}

您甚至不需要 R 模板参数。

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 应将 F... 作为模板参数,原因在本答案开头列出 (即避免对临时变量的悬空引用)

它仍然应该以 std::forward<F>(f)... 作为参数,以允许 rvalues 被转发 (即将右值移动到 function_list的元组).

这取决于 function_list 的用途。基本上有两种情况:

  1. function_list 是一个临时助手,永远不会超过它出现的语句。在这里我们可以存储对函数的引用和 perfect-forward 每个函数的引用到调用点:

    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 是一个类似于 std::bind 的 wrapper/container 对象,在这种情况下,您希望存储函数的衰减副本以避免悬挂引用和在这种情况下,perfect-forwarding 意味着将函数转发给 f_ 中衰减版本的构造函数,然后在调用点将衰减函数注入 function_list 本身的值类别:

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

    std::bind 一样,如果您确实想要存储引用,则必须使用 std::reference_wrapper 明确地这样做。

两种情况下的构造相同:

template <class... F>
constexpr auto make_function_list(F&&... f)
{
    return function_list<F...>(std::forward<F>(f)...);
}