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!
2 Answers 2
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
-qualifyapply
andoperator()
.size
should bestatic constexpr
instead ofstatic 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 prefixstd::
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 namedstd::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
forint2type<0>
and not forint2type<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 :)
-
\$\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– davidhigh2014年09月25日 13:51:04 +00:00Commented Sep 25, 2014 at 13:51
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
}
int f(int)
andint g(int)
functors isauto h = [&](int x) { return f(g(x)); };
). What you mean by "compose" should determine if this is a good solution or not. \$\endgroup\$