18
\$\begingroup\$

What is a good way to compose std::function objects in C++?

I tried the following, and it seems to work well:

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

    template<std::size_t> struct int2type{};

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

    static const size_t size = sizeof ... (Fs);
    template<typename ... Ts>
    auto apply(int2type<size-1>, Ts&& ... ts)
    {
        return std::get<size-1>(functionTuple)(std::forward<Ts>(ts)...);
    }

    template<typename ... Ts>
    auto operator()(Ts&& ... ts)
    {
        return apply(int2type<0>(), std::forward<Ts>(ts)...);
    }

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

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

With this, one can compose functions as long as the signatures fit together. Example:

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)

Comments and suggestions for improvement are welcome!

\$\endgroup\$
  • 1
    \$\begingroup\$ Can you please edit this and post something about your motivation for doing this? (because the easiest way to compose int f(int) and int g(int) functors is auto h = [&](int x) { return f(g(x)); };). What you mean by "compose" should determine if this is a good solution or not. \$\endgroup\$ – utnapistim Sep 25 '14 at 13:51
  • \$\begingroup\$ The motivation is as simple as I wrote it: compose several function objects into a single one (of course I have my specific application in mind which I guess is not important here). Until now I have not thought in terms of lambdas, so thanks for the hint, but I agree that with generic lambdas one could obtain the same generality with less (and less complicated) code. \$\endgroup\$ – davidhigh Sep 25 '14 at 14:57
8
\$\begingroup\$

Generally speaking, it is really well done, for several reasons: std::tuple often takes advantage of the empty base class optimization, which means that since you feed it lambdas, your class will often weight almost nothing, and everything is correctly forwarded. The only things I see that could be improved are the following ones:

  • You could const-qualify apply and operator().

  • size should be static constexpr instead of static const to make it even clearer that it is a compile-time constant.

  • You should be consistent when qualifying std::size_t: either use the prefix std:: or leave it, but stay consistent.

As you can see, these are really minor improvements. I also have some other remarks, but those will be opinions more than actual advice:

  • int2type kind of already exists in the standard and is named std::integral_constant. However, I will concede that it takes another template parameter for the type and that it might be too verbose for your needs.

  • I had some trouble understanding how your recursion worked because it was in ascending order. For some reason, I am more used to descending order. I would have overloaded apply for int2type<0> and not for int2type<size-1> and performed a descending recursion. That would have allowed me to write:

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

    And then, size wouldn't have had to be a member of the class anymore. But I have to admit that this is an opinion and not a guideline. Your code is good enough that I see almost nothing that could be improved :)

\$\endgroup\$
  • \$\begingroup\$ Thanks for your review (--particularly for the empty-base-class comment and std::integral_constant, didn't knew that). I will incorporate all of your suggestions, except maybe for the other recursion direction (... this I first have to figure out ;-) ). \$\endgroup\$ – davidhigh Sep 25 '14 at 13:51
3
\$\begingroup\$

For completeness, here is a revision of the above code incorporating Morwenn's thorough suggestions:

template<typename ... Fs>
struct compose_impl
{
    compose_impl(Fs&& ... fs) : functionTuple(std::forward<Fs>(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) ...);
    //possibly also
    //return compose_impl<std::decay_t<Fs> ...>(std::forward<Fs>(fs) ...);
    //                    ^^^^^^^^^^^^^^^
    //                    if you want to have copies of the
    //                    functions instead of references
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.