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

时间:2017-02-03 20:46:17

标签: c++ function c++11 template-meta-programming perfect-forwarding

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

// 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背后发生的所有类型推论和通用引用。在上面的代码中,我有三个问题:

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

注意:function_list的构造函数不应该直接调用,而make_function_list正在执行此工作。

编辑: 如果operator() function_list(此处未显示)不能保证在同一声明中被调用,这种情况是否安全?

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

3 个答案:

答案 0 :(得分:3)

  

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

通过一个例子来理解它很简单。

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

如果是(0)

    对于 rvalues
  • T被推断为T
  • T推断为左轮眼T&

(1)

的情况下
    对于 rvalues
  • T被推断为T&&
  • T推断为左轮眼T&

如您所见,两者之间的唯一区别是如何推断 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否则会保留左值

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)...作为允许 rvalues 转发的参数(即将rvalues移动到function_list的元组中)

答案 1 :(得分:2)

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

在下一行,它现在是悬挂参考。

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

通常,如果您要返回T&&,则希望将其作为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()需要一个实例。类型名称不是实例。

答案 2 :(得分:0)

这取决于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明确地执行此操作。

  3. 两种情况下的构造都是相同的:

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