完美转发函数以构建函数列表类

Perfect forwarding of functions to build a function list class

本文关键字:函数 列表 构建 转发 完美      更新时间:2023-10-16

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

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

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

  • _f应该是std::tuple<F...>型还是std::tuple<F&&...>型(为什么?
  • 是否可以推断模板参数列表中R的返回类型(因为手动而不是auto/decltype(auto)将有助于了解正在发生的事情)
  • 在制作器中,function_list模板参数应该是什么:decltype(std::forward<F>(f)...)FF&&...(为什么?

注意: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)...);
}
但我

并不完全理解std::forward和通用引用背后发生的所有类型推导。

通过一个例子很容易理解。

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

(0)的情况下:

  • T被推算为右值T
  • T被推算为左值T&

(1)的情况下:

  • T被推算为右值T&&
  • T被推算为左值T&

如您所见,两者之间的唯一区别是如何推导右值

关于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)的情况下:

  • 如果T被推导为T,则x被投射到T&&

  • 否则x保持稳定值

std::forward基本上保留了x的类型类别,通过查看如何推断T


_f应属于std::tuple<F...>型或std::tuple<F&&...>

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

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


是否可以推断出模板参数列表中的返回类型R

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

在制作器中,function_list模板参数应该是什么:decltype(std::forward<F>(f)...)FF&&...

function_list应将F...作为模板参数,原因在此答案的开头列出(即避免悬而未决地引用临时)。

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

组)。

如果它们是F&&的,那么如果你将一个临时传递给make_function_list,则包含tuple的返回类将存储对传递给make_function_list的临时的右值引用。

在下一行,它现在是一个悬而未决的引用。

这在大多数用例中似乎很糟糕。 这在所有用例中实际上并不坏;forward_as_tuple这样做。 但这样的用例不是一般的用例。 该图案非常脆弱和危险。

通常,如果您要返回T&&,则希望将其作为T返回。 这可能会导致对象的副本;但另一种选择是悬而未决的参考地狱。

这为我们提供了:

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的 ctorexplicit,因为在 1 参数的情况下,它演变成一个相当贪婪的隐式转换构造函数。 这可以修复,但需要付出更多的努力。

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

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

  1. function_list是一个临时帮助程序,它永远不会超过它出现的语句。在这里,我们可以存储对函数的引用,并将每个函数完美转发到调用点:

    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的包装器/容器对象,在这种情况下,您需要存储函数的衰减副本以避免悬而未决的引用,在这种情况下,完美转发意味着将函数转发给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)...);
}