【问题标题】:decltype for the return type of recursive variadic function templatedecltype 用于递归可变参数函数模板的返回类型
【发布时间】:2015-02-17 21:58:21
【问题描述】:

给出以下代码(取自here):

#include <cstddef>
#include <type_traits>
#include <tuple>
#include <iostream>
#include <utility>
#include <functional>

template<typename ... Fs>
struct compose_impl
{
    compose_impl(Fs&& ... fs) : functionTuple(std::forward_as_tuple(fs ...)) {}

    template<size_t N, typename ... Ts>
    auto apply(std::integral_constant<size_t, N>, Ts&& ... ts) const
    {
        return apply(std::integral_constant<size_t, N - 1>(), std::get<N>  (functionTuple)(std::forward<Ts>(ts)...));
    }

    template<typename ... Ts>
    auto apply(std::integral_constant<size_t, 0>, Ts&& ... ts) const
    {
        return std::get<0>(functionTuple)(std::forward<Ts>(ts)...);
    }

    template<typename ... Ts>
    auto operator()(Ts&& ... ts) const
    {
         return apply(std::integral_constant<size_t, sizeof ... (Fs) - 1>(), std::forward<Ts>(ts)...);
    }

    std::tuple<Fs ...> functionTuple;
};

template<typename ... Fs>
auto compose(Fs&& ... fs)
{
     return compose_impl<Fs ...>(std::forward<Fs>(fs) ...);
}

int main ()
{
    auto f1 = [](std::pair<double,double> p) {return p.first + p.second;    };
    auto f2 = [](double x) {return std::make_pair(x, x + 1.0); };
    auto f3 = [](double x, double y) {return x*y; };
    auto g = compose(f1, f2, f3);

    std::cout << g(2.0, 3.0) << std::endl;   //prints '13', evaluated as (2*3) + ((2*3)+1)
    return 0;
}

上面的代码在 C++14 中工作。我在使它适用于 C++11 时遇到了一些麻烦。我尝试为所涉及的函数模板正确提供返回类型,但没有取得多大成功,例如:

template<typename... Fs>
struct compose_impl
{
    compose_impl(Fs&&... fs) : func_tup(std::forward_as_tuple(fs...)) {}

    template<size_t N, typename... Ts>
    auto apply(std::integral_constant<size_t, N>, Ts&&... ts) const -> decltype(std::declval<typename std::tuple_element<N, std::tuple<Fs...>>::type>()(std::forward<Ts>(ts)...))
    // -- option 2. decltype(apply(std::integral_constant<size_t, N - 1>(), std::declval<typename std::tuple_element<N, std::tuple<Fs...>>::type>()(std::forward<Ts>(ts)...)))
    {
         return apply(std::integral_constant<size_t, N - 1>(), std::get<N>(func_tup)(std::forward<Ts>(ts)...));
    }

    using func_type = typename std::tuple_element<0, std::tuple<Fs...>>::type;
    template<typename... Ts>
    auto apply(std::integral_constant<size_t, 0>, Ts&&... ts) const -> decltype(std::declval<func_type>()(std::forward<Ts>(ts)...))
    {
        return std::get<0>(func_tup)(std::forward<Ts>(ts)...);
    }

    template<typename... Ts>
    auto operator()(Ts&&... ts) const -> decltype(std::declval<func_type>()(std::forward<Ts>(ts)...))
    // -- option 2. decltype(apply(std::integral_constant<size_t, sizeof...(Fs) - 1>(), std::forward<Ts>(ts)...))
    {
        return apply(std::integral_constant<size_t, sizeof...(Fs) - 1>(), std::forward<Ts>(ts)...);
    }

    std::tuple<Fs...> func_tup;
};

template<typename... Fs>
auto compose(Fs&&... fs) -> decltype(compose_impl<Fs...>(std::forward<Fs>(fs)...))
{
   return compose_impl<Fs...>(std::forward<Fs>(fs)...);
}

对于上述 clang(3.5.0) 给我以下错误:

func_compose.cpp:79:18: error: no matching function for call to object of type 'compose_impl<(lambda at func_compose.cpp:65:15) &, (lambda at func_compose.cpp:67:15) &,
  (lambda at func_compose.cpp:68:15) &>'
std::cout << g(2.0, 3.0) << std::endl;   //prints '13', evaluated as (2*3) + ((2*3)+1)
             ^
 func_compose.cpp:31:10: note: candidate template ignored: substitution failure [with Ts = <double, double>]: no matching function for call to object of type
  '(lambda at func_compose.cpp:65:15)'
 auto operator()(Ts&&... ts) /*const*/ -> decltype(std::declval<func_type>()(std::forward<Ts>(ts)...))
     ^                                            ~~~
1 error generated.

如果我尝试“选项 2”。我得到了几乎相同的错误。

除了它看起来非常冗长之外,我似乎也无法正确理解它。谁能提供一些关于我做错了什么的见解? 有没有更简单的方法来提供返回类型?

【问题讨论】:

    标签: c++ c++11 variadic-templates c++14 decltype


    【解决方案1】:

    第一个选项的错误消息是由于

    std::declval<func_type>()(std::forward<Ts>(ts)...)
    

    你正试图用两个 double 类型的参数(传递给 operator() 的参数)调用 f1 函子,但它需要一个 std::pairfunc_type 指第一个的类型元组中的函子)。

    关于选项 2,它不编译的原因是尾随返回类型是函数声明符的一部分,并且直到看到声明符的末尾才认为函数已声明,因此您不能使用 @ apply 的第一个声明的尾随返回类型中的 987654329@。


    我相信您现在很高兴知道为什么您的代码无法编译,但我想如果您有一个可行的解决方案,您会更高兴。

    我认为首先需要澄清一个基本事实:compose_implapplyoperator() 模板的所有特化 具有相同的返回类型 - 的返回类型第一个函子,在本例中为 f1

    有几种方法可以获取该类型,但以下是快速破解方法:

    #include <cstddef>
    #include <type_traits>
    #include <tuple>
    #include <iostream>
    #include <utility>
    #include <functional>
    
    template<typename> struct ret_hlp;
    
    template<typename F, typename R, typename... Args> struct ret_hlp<R (F::*)(Args...) const>
    {
        using type = R;
    };
    
    template<typename F, typename R, typename... Args> struct ret_hlp<R (F::*)(Args...)>
    {
        using type = R;
    };
    
    template<typename ... Fs>
    struct compose_impl
    {
        compose_impl(Fs&& ... fs) : functionTuple(std::forward_as_tuple(fs ...)) {}
    
        using f1_type = typename std::remove_reference<typename std::tuple_element<0, std::tuple<Fs...>>::type>::type;
        using ret_type = typename ret_hlp<decltype(&f1_type::operator())>::type;
    
        template<size_t N, typename ... Ts>
        ret_type apply(std::integral_constant<size_t, N>, Ts&& ... ts) const
        {
            return apply(std::integral_constant<size_t, N - 1>(), std::get<N>  (functionTuple)(std::forward<Ts>(ts)...));
        }
    
        template<typename ... Ts>
        ret_type apply(std::integral_constant<size_t, 0>, Ts&& ... ts) const
        {
            return std::get<0>(functionTuple)(std::forward<Ts>(ts)...);
        }
    
        template<typename ... Ts>
        ret_type operator()(Ts&& ... ts) const
        {
             return apply(std::integral_constant<size_t, sizeof ... (Fs) - 1>(), std::forward<Ts>(ts)...);
        }
    
        std::tuple<Fs ...> functionTuple;
    };
    
    template<typename ... Fs>
    compose_impl<Fs ...> compose(Fs&& ... fs)
    {
         return compose_impl<Fs ...>(std::forward<Fs>(fs) ...);
    }
    
    int main ()
    {
        auto f1 = [](std::pair<double,double> p) {return p.first + p.second;    };
        auto f2 = [](double x) {return std::make_pair(x, x + 1.0); };
        auto f3 = [](double x, double y) {return x*y; };
        auto g = compose(f1, f2, f3);
    
        std::cout << g(2.0, 3.0) << std::endl;   //prints '13', evaluated as (2*3) + ((2*3)+1)
        return 0;
    }
    

    注意事项:

    • 它可以在 C++11 模式下的 GCC 4.9.1 和 Clang 3.5.0 以及 Visual C++ 2013 上编译和运行。
    • 正如所写,ret_hlp 仅处理声明其 operator() 的函数对象类型,类似于 lambda 闭包类型,但它可以轻松扩展到几乎任何其他内容,包括普通函数类型。
    • 我尽量少改动原代码;我认为关于该代码需要提到一个重要的一点:如果给compose 提供左值参数(如本例所示),compose_impl 内的functionTuple 将存储这些参数的引用 .这意味着只要使用复合函子,原始函子就必须可用,否则您将有悬空引用。

    编辑:根据评论中的要求,这是关于最后一个注释的更多信息:

    这种行为是由于转发引用的工作方式 - composeFs&amp;&amp; ... 函数参数。如果您有一个 F&amp;&amp; 形式的函数参数,正在对其进行模板参数推导(就像这里一样),并且为该参数提供了一个 A 类型的参数,那么:

    • 如果参数表达式是一个右值F 被推导出为A,并且,当替换回函数参数时,它给出A&amp;&amp;(例如,这会发生如果您将 lambda 表达式 直接 作为参数传递给 compose);
    • 如果参数表达式是一个左值F 被推导出为A&amp;,并且,当替换回函数参数时,它给出A&amp; &amp;&amp;,根据它产生A&amp; reference collapsing 规则(这是当前示例中发生的情况,因为 f1 和其他都是左值)。

    因此,在当前示例中,compose_impl 将使用推导出的模板参数进行实例化,例如(使用 lambda 闭包类型的发明名称)

    compose_impl<lambda_1_type&, lambda_2_type&, lambda_3_type&>
    

    这反过来会使functionTuple 具有类型

    std::tuple<lambda_1_type&, lambda_2_type&, lambda_3_type&>
    

    如果您将 lambda 表达式直接作为参数传递给 compose,那么根据上述情况,functionTuple 将具有类型

    std::tuple<lambda_1_type, lambda_2_type, lambda_3_type>
    

    因此,只有在后一种情况下,元组才会存储函数对象的副本,从而使组合的函数对象类型自包含。

    现在,这不是好坏的问题;这是一个你想要什么的问题。

    如果您希望组合对象始终是自包含的(存储仿函数的副本),那么您需要摆脱这些引用。一种方法是使用std::decay,因为它不仅仅是删除引用——它还处理函数到指针的转换,如果你想扩展compose_impl 以便能够处理普通的功能。

    最简单的方法是更改​​functionTuple 的声明,因为它是您在当前实现中唯一关心引用的地方:

    std::tuple<typename std::decay<Fs>::type ...> functionTuple;
    

    结果是函数对象将始终在元组内被复制或移动,因此即使在原始组件被破坏后,生成的组合函数对象也可以使用。

    哇,这太长了;也许你不应该说“精心制作”:-)。


    编辑 2 来自 OP 的第二条评论:是的,代码原样,没有 std::decay (但扩展为正确确定 ret_type 用于普通函数参数,如你所说)将处理普通函数,但是小心:

    int f(int) { return 7; }
    
    int main()
    {
        auto c1 = compose(&f, &f); //Stores pointers to function f.
        auto c2 = compose(f, f); //Stores references to function f.
        auto pf = f; //pf has type int(*)(int), but is an lvalue, as opposed to &f, which is an rvalue.
        auto c3 = compose(pf, pf); //Stores references to pointer pf.
        std::cout << std::is_same<decltype(c1.functionTuple), std::tuple<int(*)(int), int(*)(int)>>::value << '\n';
        std::cout << std::is_same<decltype(c2.functionTuple), std::tuple<int(&)(int), int(&)(int)>>::value << '\n';
        std::cout << std::is_same<decltype(c3.functionTuple), std::tuple<int(*&)(int), int(*&)(int)>>::value << '\n';
    }
    

    c3 的行为可能不是您想要的或人们所期望的。更不用说所有这些变体可能会混淆您确定ret_type 的代码。

    使用std::decay,所有三个变体都存储指向函数f的指针。

    【讨论】:

    • 我会将其标记为已接受,因为它引导我朝着正确的方向前进。您能否详细说明第三条评论?你有没有更好的方法来实现它?
    • 感谢您的详细说明:)。附带说明一下,它的实现,连同返回类型的正确 function_trait,也处理普通函数。
    猜你喜欢
    • 2023-01-10
    • 2011-04-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-18
    • 2018-03-31
    相关资源
    最近更新 更多