cd /news/developer-tools/function-composition-from-c-17-to-c-… · home topics developer-tools article
[ARTICLE · art-31023] src=freshsources.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Function Composition from C++17 to C++23

C++23 code capsules trace the evolution of function composition from C++17 to C++23, building a reusable Composer class that grows cleaner with each standard. The journey highlights how C++ adopted functional programming concepts like right folds, moving from a function-type parameterization in C++17 to a value-type parameterization in C++20, with further improvements expected in C++23.

read5 min views1 publishedJun 17, 2026

C++23 Code Capsules

Mathematicians and programmers alike have always known that functions are things you can do things with, not just things you call. One of the most natural operations on functions is composition: given f and g, form the new function (f ∘ g) where (f ∘ g)(x) = f(g(x)). Chain enough of these together and you have a pipeline — a sequence of transformations applied one after another.

Functional programmers have known this for decades. In Standard ML, folding a list of functions over an initial value is idiomatic and concise:

(* ML: apply a list of functions right-to-left *)
fun compose fs x = foldr (fn (f, acc) => f acc) x fs

foldr

processes the list from the right, threading an accumulator through each function in turn. The result is function composition expressed as a fold — (f_1(f_2(...f_n(x)...))) — and the combining function takes (element, accumulator)

in right-to-left order.

C++, being based on a procedural language that put performance first, took a longer road to arrive at the same idea. But arrive it did. This capsule traces that journey across three standards, building a reusable Composer

class that grows cleaner at each step.

Here is a first cut, written in C++17:

// Makes the function type generic
#include <algorithm>
#include <functional>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;

template<typename Fun>
class Composer {
    vector<Fun>& funs;
public:
    Composer(vector<Fun>& fs) : funs(fs) {}
    using T = typename Fun::result_type;
    T operator()(T x) const {
        auto apply = [](T sofar, Fun f){ return f(sofar); };
        return accumulate(rbegin(funs), rend(funs), x, apply);
    }
};

struct g {
    double operator()(double x) { return x * x; }
};

int main() {
    auto f = [](double x){ return x / 2.0; };
    using Fun = function<double(double)>;
    vector<Fun> funs{f, g(), [](double x){ return x + 1.0; }};
    Composer<Fun> comp(funs);
    cout << comp(2.0) << "\n";          // 4.5

    using Fun2 = function<string(const string&)>;
    vector<Fun2> funs2{
        [](const string& s){ return s + "s"; },
        [](const string& s){ return s + "'"; }
    };
    Composer<Fun2> comp2(funs2);
    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 folding right — it applies the last function first, then the second-to-last, and so on. This is exactly ML’s foldr

in disguise, expressed through std::accumulate

and reversed iterators. It works, but the disguise is unfortunate: the intent is a right fold, but it isn’t crystal clear in the code.

There is also a more practical problem. Composer

is parameterized on the function type Fun

, and it extracts the value type via Fun::result_type

. That member only exists on std::function

, not on raw lambdas or function objects. The using Fun = function<double(double)>

in main

is not incidental — it is required. The class forces its clients to wrap their callables.

C++20 does not change the algorithm, but it invites a rethinking of the interface. The right abstraction for single-valued functions is not “a composer parameterized on a function type” — it is “a composer parameterized on a value type.” The functions are an implementation detail; what matters to the caller is the type they are transforming.

Flipping the template parameter from Fun

to T

gives us this:

template<typename T>
class Composer {
    vector<function<T(T)>> funs;
public:
    Composer(vector<function<T(T)>> fs) : funs(move(fs)) {}

    T operator()(T x) const {
        auto apply = [](T acc, auto f){ return f(acc); };
        return accumulate(rbegin(funs), rend(funs), x, apply);
    }
};

Now main

reads naturally:

Composer<double> comp({ ... });
Composer<string> comp2({ ... });

Composer<double>

means what it says: a composition of functions on double

. The std::function

wrapping still happens, but it is now an internal detail of the class, not something the caller needs to explicitly name. C++20 concepts could further constrain T

(to require copyability, say, or to express the same input as output type requirement), but for a capsule of this size the improvement in readability already tells the story.

C++23 brings two things that complete the picture: std::ranges::fold_right

, and a usable implementation of modules. (I’ll admit that this problem is small enough to not require a module; in fact Composer

could be a part of a larger module, but indulge me here. :-)

fold_right

replaces the accumulate(rbegin, rend, ...)

idiom with something that names its intent directly:

T operator()(T x) const {
    return std::ranges::fold_right(funs, x, [](auto f, auto acc){ return f(acc); });
}

Notice the argument order in the lambda: (element, accumulator)

. This is the same order as ML’s foldr

combining function — fn (f, acc) => f acc

. That is not a coincidence. C++ has absorbed the idea, and the interface reflects it.

The full module interface file:

// composer.cppm
export module composer;

import std;

export template<typename T>
class Composer {
    std::vector<std::function<T(T)>> funs;
public:
    Composer(std::vector<std::function<T(T)>> fs) : funs(std::move(fs)) {}

    T operator()(T x) const {
        return std::ranges::fold_right(funs, x, [](auto f, auto acc){ return f(acc); });
    }
};

And the test driver that consumes it:

// compose23.cpp
import composer;
import std;

int main() {
    Composer<double> comp({
        [](double x){ return x / 2.0; },
        [](double x){ return x * x; },
        [](double x){ return x + 1.0; }
    });
    std::cout << comp(2.0) << "\n";         // 4.5

    Composer<std::string> comp2({
        [](std::string s){ return s + "s"; },
        [](std::string s){ return s + "'"; }
    });
    std::cout << comp2("Vernor") << "\n";   // Vernor's
}

Composer

is a natural fit for a module: it is self-contained, has no platform dependencies, and exports exactly one thing. The import std;

in the test driver replaces a half-dozen headers. The result is as clean as the class deserves.

We started with a right fold expressed as accumulate

over reversed iterators — a correct but oblique encoding of an idea that ML stated directly in the 1970s. Over three standards, C++ acquired the vocabulary to say the same thing clearly: a value-typed interface, a named fold_right

, a module boundary that packages the abstraction for reuse.

This is the direction modern C++ has been moving for some time. Ranges, folds, std::function

, concepts, modules — these are not disconnected features. They are the pieces of a language that is gradually adopting powerful ideas in its own idiom, an idiom that has had a sometimes-hard-to-follow syntactic path, but has delivered performance and code compatibility without peer.

What goes around comes around.

── more in #developer-tools 4 stories · sorted by recency
── more on @c++17 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/function-composition…] indexed:0 read:5min 2026-06-17 ·