composer_blog_post
Composing Functions in Modern<br>C++
C++23 Code Capsules
Mathematicians and programmers alike have always known that functions<br>are things you can do things with, not just things you<br>call. One of the most natural operations on functions is<br>composition: given f and g, form the new function (f ∘ g) where (f ∘<br>g)(x) = f(g(x)). Chain enough of these together and you have a pipeline<br>— a sequence of transformations applied one after another.
Functional programmers have known this for decades. In Standard ML,<br>folding a list of functions over an initial value is idiomatic and<br>concise:
(* ML: apply a list of functions right-to-left *)<br>fun compose fs x = foldr (fn (f, acc) => f acc) x fs
foldr processes the list from the right, threading an<br>accumulator through each function in turn. The result is function<br>composition expressed as a fold — \(f_1(f_2(...f_n(x)...))\) — and the<br>combining function takes (element, accumulator) in<br>right-to-left order.
C++, being based on a procedural language that put performance first,<br>took a longer road to arrive at the same idea. But arrive it did. This<br>capsule traces that journey across three standards, building a reusable<br>Composer class that grows cleaner at each step.
C++17: A Working Solution
Here is a first cut, written in C++17:
// Makes the function type generic<br>#include<br>#include<br>#include<br>#include<br>#include<br>using namespace std;
templatetypename Fun><br>class Composer {<br>vectorFun>& funs;<br>public:<br>Composer(vectorFun>& fs) : funs(fs) {}<br>using T = typename Fun::result_type;<br>T operator()(T x) const {<br>auto apply = [](T sofar, Fun f){ return f(sofar); };<br>return accumulate(rbegin(funs), rend(funs), x, apply);<br>};
struct g {<br>double operator()(double x) { return x * x; }<br>};
int main() {<br>auto f = [](double x){ return x / 2.0; };<br>using Fun = functiondouble(double)>;<br>vectorFun> funs{f, g(), [](double x){ return x + 1.0; }};<br>ComposerFun> comp(funs);<br>cout comp(2.0) "\n"; // 4.5
using Fun2 = functionstring(const string&)>;<br>vectorFun2> funs2{<br>[](const string& s){ return s + "s"; },<br>[](const string& s){ return s + "'"; }<br>};<br>ComposerFun2> comp2(funs2);<br>cout comp2("Vernor") "\n"; // Vernor's
The core of the class is this line:
return accumulate(rbegin(funs), rend(funs), x, apply);
Walking the vector in reverse and folding left is equivalent to<br>folding right — it applies the last function first, then the<br>second-to-last, and so on. This is exactly ML’s foldr in<br>disguise, expressed through std::accumulate and reversed<br>iterators. It works, but the disguise is unfortunate: the intent is a<br>right fold, but it isn’t crystal clear in the code.
There is also a more practical problem. Composer is<br>parameterized on the function type Fun, and it<br>extracts the value type via Fun::result_type. That member<br>only exists on std::function, not on raw lambdas or<br>function objects. The<br>using Fun = function in<br>main is not incidental — it is required. The class forces<br>its clients to wrap their callables.
C++20: A Cleaner Interface
C++20 does not change the algorithm, but it invites a rethinking of<br>the interface. The right abstraction for single-valued functions is not<br>“a composer parameterized on a function type” — it is “a composer<br>parameterized on a value type.” The functions are an implementation<br>detail; what matters to the caller is the type they are<br>transforming.
Flipping the template parameter from Fun to<br>T gives us this:
templatetypename T><br>class Composer {<br>vectorfunctionT(T)>> funs;<br>public:<br>Composer(vectorfunctionT(T)>> fs) : funs(move(fs)) {}
T operator()(T x) const {<br>auto apply = [](T acc, auto f){ return f(acc); };<br>return accumulate(rbegin(funs), rend(funs), x, apply);<br>};
Now main reads naturally:
Composerdouble> comp({ ... });<br>Composerstring> comp2({ ... });
Composer means what it says: a composition<br>of functions on double. The std::function<br>wrapping still happens, but it is now an internal detail of the class,<br>not something the caller needs to explicitly name. C++20 concepts could<br>further constrain T (to require copyability, say, or to<br>express the same input as output type requirement), but for a capsule of<br>this size the improvement in readability already tells the story.
C++23: Saying What You Mean
C++23 brings two things that complete the picture:<br>std::ranges::fold_right, and a usable implementation of<br>modules. (I’ll admit that this problem is small enough to not<br>require a module; in fact Composer could be a part of a<br>larger module, but indulge me here. :-)
fold_right replaces the<br>accumulate(rbegin, rend, ...) idiom with something that<br>names its intent directly:
T operator()(T x) const {<br>return std::ranges::fold_right(funs, x, [](auto f, auto acc){ return f(acc); });
Notice the argument order in the lambda:<br>(element, accumulator). This is the same order as ML’s<br>foldr combining function —<br>fn (f, acc) => f acc. That is not a coincidence. C++ has<br>absorbed the idea, and the interface reflects it.
The full module interface file:
// composer.cppm<br>export module...