HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppModerate

C++ function composition

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
functioncompositionstackoverflow

Problem

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

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

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

    template struct int2type{};

    template
    auto apply(int2type, Ts&& ... ts)
    {
        return std::get(functionTuple)(apply(int2type(),std::forward(ts)...));
    }

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

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

    std::tuple functionTuple;
};

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


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

auto f1 = [](std::pair 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!

Solution

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 weigh 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 and not for int2type and performed a descending recursion. That would have allowed me to write:

template
auto operator()(Ts&& ... ts)
{
    return apply(int2type(), std::forward(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 :)

Code Snippets

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

Context

StackExchange Code Review Q#63841, answer score: 12

Revisions (0)

No revisions yet.