简体   繁体   中英

decltype for the return type of recursive variadic function template

Given the following code(taken from 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;
}

The code above works in C++14. I'm having some trouble making it work for C++11. I tried to properly provide the return types for the function templates involved but without much success eg:

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

For the above clang(3.5.0) gives me the following error:

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.

If I try "option 2." I get pretty much the same error.

Apart from the fact that it looks very verbose I also cannot seem to get it right. Could anyone provide some insight in what am I doing wrong? Is there any simpler way to provide the return types?

The error message for your first option is due to the fact that in

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

you're trying to call the f1 functor with two arguments of type double (the ones passed to operator() ), but it takes a std::pair ( func_type refers to the type of the first functor in the tuple).

Regarding option 2, the reason it doesn't compile is that the trailing return type is part of the function declarator and the function is not considered declared until the end of the declarator has been seen, so you can't use decltype(apply(...)) in the trailing return type of the first declaration of apply .


I'm sure you're now very happy to know why your code doesn't compile, but I guess you'd be even happier if you had a working solution.

I think there's an essential fact that needs to be clarified first: all specializations of the apply and operator() templates in compose_impl have the same return type - the return type of the first functor, f1 in this case.

There are several ways to get that type, but a quick hack is the following:

#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;
}

Notes:

  • It compiles and works on GCC 4.9.1 and Clang 3.5.0 in C++11 mode, and on Visual C++ 2013.
  • As written, ret_hlp only handles function object types that declare their operator() similarly to lambda closure types, but it can be easily extended to pretty much anything else, including plain function types.
  • I tried to change the original code as little as possible; I think there's one important bit that needs to be mentioned regarding that code: if compose is given lvalue arguments (as in this example), functionTuple inside compose_impl will store references to those arguments. This means the original functors need to be available for as long as the composite functor is used, otherwise you'll have dangling references.

EDIT: Here's more info on the last note, as requested in the comment:

That behaviour is due to the way forwarding references work - the Fs&& ... function parameters of compose . If you have a function parameter of the form F&& for which template argument deduction is being done (as it is here), and an argument of type A is given for that parameter, then:

  • if the argument expression is an rvalue , F is deduced as A , and, when substituted back into the function parameter, it gives A&& (for example, this would happen if you passed a lambda expression directly as the argument to compose );
  • if the argument expression is an lvalue , F is deduced as A& , and, when substituted back into the function parameter, it gives A& && , which yields A& according to the reference collapsing rules (this is what happens in the current example, as f1 and the others are lvalues).

So, in the current example, compose_impl will be instantiated using the deduced template arguments as something like (using invented names for lambda closure types)

compose_impl<lambda_1_type&, lambda_2_type&, lambda_3_type&>

which in turn will make functionTuple have the type

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

If you'd pass the lambda expressions directly as arguments to compose , then, according to the above, functionTuple will have the type

std::tuple<lambda_1_type, lambda_2_type, lambda_3_type>

So, only in the latter case will the tuple store copies of the function objects, making the composed function object type self-contained.

Now, it's not a question of whether this is good or bad; it's rather a question of what you want.

If you want the composed object to always be self-contained (store copies of the functors), then you need to get rid of those references. One way to do it here is to use std::decay , as it does more than remove references - it also handles function-to-pointer conversions, which comes in handy if you want to extend compose_impl to be able to also handle plain functions.

The easiest way is to change the declaration of functionTuple , as it's the only place where you care about references in the current implementation:

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

The result is that the function objects will always be copied or moved inside the tuple, so the resulting composed function object can be used even after the original components have been destructed.

Wow, this got long; maybe you shouldn't have said 'elaborate' :-).


EDIT 2 for the second comment from the OP: Yes, the code as it is, without the std::decay (but extended to properly determine ret_type for plain function arguments, as you said) will handle plain functions, but be careful:

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';
}

The behaviour of c3 is probably not what you want or what one would expect. Not to mention all these variants will likely confuse your code for determining ret_type .

With the std::decay in place, all three variants store pointers to function f .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM