c++ - 递归可变参数函数模板的返回类型的decltype

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

给定以下代码(取自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”。我得到几乎相同的错误。

除了看起来很冗长之外,我似乎也无法做到这一点。谁能提供一些我做错了什么的见解?
有没有更简单的方法来提供返回类型?

最佳答案

您的第一个选择的错误消息是由于以下事实

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

您正在尝试使用两个类型为f1的参数(传递给double的参数)调用operator()仿函数,但它需要一个std::pair(func_type表示元组中第一个仿函数的类型)。

关于选项2,之所以无法编译,是因为尾随返回类型是函数声明器的一部分,并且直到看到该声明器的末尾,该函数才被认为是声明了,因此您不能在尾部使用decltype(apply(...))返回apply的第一个声明的类型。

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

我认为有一个基本事实需要首先阐明:apply operator()compose_impl模板的所有特殊化都具有相同的返回类型-在这种情况下,第一个仿函数的返回类型为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提供了左值参数(如本例所示),则functionTuple中的compose_impl将存储对这些参数的引用。这意味着只要使用了复合仿函数,原始仿函数就需要一直可用,否则您将有悬挂的引用。


  • 编辑:这是根据注释的要求提供的关于最后一个音符的更多信息:

    该行为归因于转发引用的工作方式-Fs&& ...compose函数参数。如果您具有F&&形式的函数参数,正在为其执行模板参数推导(如此处所示),并且为此参数给出了A类型的参数,则:
  • 如果参数表达式是一个右值,则将F推导为A,当替换回函数参数时,它会给出A&&(例如,如果您直接将lambda表达式作为参数传递给compose,则会发生这种情况);
  • 如果参数表达式是一个左值,则将F推导为A&,并将其替换回函数参数后,它会给出A& &&,根据reference collapsing规则产生A&(这是当前示例中的情况,如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函数的指针。

    关于c++ - 递归可变参数函数模板的返回类型的decltype,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28571902/

    相关文章:

    c++ - 使交换更快、更容易使用和异常安全

    c++ - std::is_same 结果与左值和右值引用

    c++ - 模板推导不适用于函数指针引用

    c++ - 事件类需要更新

    c++ - 重新排序可变参数

    c++ - 检查点是否在 vector 内

    c++ - 如何在 dlib 正面面部检测器中拆分级联级别?

    c++ - stat64 在 64 位 ubuntu 上返回 32 位 st_size

    c++ - 对参数依赖查找和友元函数定义的混淆

    c++ - 具有函数、输入和输出类型的可变参数模板