That’s Not Functional — It’s Just Bad

Rico Mariani
6 min readOct 23, 2023

--

I have been ragging on “Modern C++” for months now and I have a new one.

People often reach for C++ lambdas and funky compositions and call them “better” because it’s “functional”. This is getting tiresome.

It is possible to write solid functional style code in C++, or almost any language for that matter, because the principles are just that simple. However, the code people write and call functional is often nothing of the kind — and gets none of the benefits. That stuff is just annoying and hard to read.

I asked an AI “What are the key tenets of functional programming” because I wanted to make sure I covered a breadth of issues. The AI is italic

Pure Functions:

Functions in functional programming are pure, which means they have no side effects and always produce the same output for the same input. They don’t rely on external state or data, making them predictable and easy to reason about.

C++ Reality: It seems the first thing people do in C++ in functional style is start writing lambdas because functional. Then, in that lambda, they capture and mutate whatever they like, often a God object. A lambda is not an anonymous function it’s an anonymous class. If you capture state that provides access to side-effects, or capture mutable state, your “purity” is totally gone.

Immutable Data

Functional programming emphasizes the use of immutable data structures. Once data is created, it cannot be changed. Instead, you create new data structures when modifications are necessary. This immutability helps prevent unintended side effects and makes code more thread-safe.

C++ Reality: First, see above, lambdas readily break the immutability pattern. Second programmers like to get their immutability by sprinkling const all over. C++ const on something doesn’t mean the object is immutable. Putting aside the fact that in C++ you can cast away const, const at best means that you can’t change the value in question with that particular variable. It says nothing about what other functions can do. You may even be able to change the const object with some other variable you have in your hands because of aliasing or reachability. This matters! Now, let’s think about the fact that std::string is mutable and it’s everywhere. If const std::string& doesn’t seem so safe now, that’s good, it shouldn’t. C++ “functional” code is frequently filled with mutability cheats and mutability hazards.

First-Class and Higher-Order Functions

In functional programming, functions are treated as first-class citizens. You can pass functions as arguments to other functions, return them from functions, and store them in data structures. Functions that operate on other functions are called higher-order functions.

C++ Reality: The sins committed by crazy nesting of lambdas are beyond counting. While it’s true that functions can be passed as arguments (function pointers and lambdas) this doesn’t mean that it’s a good idea to do so. Even in fully functional languages! In the old days when we wanted to call a function we just, you know, called it. Now we have to write an interim functor applier because calling our own functions is not in vogue. So, you get stuff like this:

// this is obviously a fabrication to make a point but, alas,
// it's not so very far from actual code I've seen
void oh_boy_transform() {
std::vector<int> nums = {1, 2, 3, 4, 5};
auto square = [](int x) { return x * x; };
std::transform(nums.begin(), nums.end(), nums.begin(), square);

for (int number : nums) {
std::cout << number << " ";
}
}

That may seem ridiculous but actually I’ve seen far worse because people just love to chain these together. And the square function provided here is positively dreamy. What if we instead had captured a little state? And the above is actually mutating the list in place… because of course that’s what you do in functional programming right?

We could be much more functional just the old-fashioned way… you know, with functions.

int square(int x) { return x*x; }

void old_school_functional() {
std::vector<int> nums = {1, 2, 3, 4, 5};
for (int number : nums) {
std::cout << square(number) << " ";
}
}

Composition of functions via function variables is useful, but it’s also easily abused — and it’s often overkill. I feel like sometimes people have this deep desire to become framework developers rather than actually solving the problem in front of them.

Recursion

Functional programming favors recursion over loops for control flow. Recursion can replace imperative constructs like for and while loops, making code more declarative and often more elegant.

C++ Reality: People play fast and loose with this and I think actually favoring recursion over loops is not always helpful for clarity — and is often a big hinderance. “Walking” data structures (with iterators) is a super-powerful thing to do and one of the most common patterns in Computer Science is to walk a data structure and accumulate some result. It could be a selection or an aggregation or a combination of those things. The result of the walk is “pure”, but the interim steps can be stateful. Recursion can help you with parallelism, but generally not without complex orchestration as well. The best parallelism metaphors often look more like pipelines or like SIMD than they do like functional recursion. In practice though, this idea of using recursion over loops is ignored a lot in C++. People love for (foo : foos) {..} and so that’s what they do.

No State and Side Effects

Functional programs strive to minimize or eliminate mutable state and side effects. This results in code that is easier to reason about, test, and debug. When state is needed, it is isolated and controlled rigorously.

C++ Reality: We’ve talked about mutability a little bit already, so I won’t double dip. But suffice to say most C++ code lives in a world that’s full of side-effects. Indeed, some would argue that the side-effects of the average C++ program are the entire of point running the code. When I was doing complex UI work, I readily adopted the key Reactive UI patterns without even using a framework: compute the next state, atomically commit that state, switch your UI to that state, repeat. I adopted them because they give excellent results. And Reactive is a functional pattern — it tries to isolate the side effects to one stage where the state is committed and then a rendering stage that actually does the UI. This works and eliminates the crazy dependencies often found in UI code. State minimization is a good idea.

But what actually happens in C++ code is that people flow file handles and response objects all over the place and then they append to them where they feel like. This boils the “functional” right out of the code.

Putting your code in a lambda doesn’t automatically make it “functional”.

Conclusion

You can apply functional principles in many languages, and they can lead to maintainable, reliable, and parallelizable code. But unsurprisingly you have to actually apply the principles to get the benefits.

The C++ reality is that functional-looking patterns are easily misapplied such that you get none of the benefits discussed above. Functional-looking C++ code is frequently not functional at all — it’s just chains of nested lambdas that are impossible to profile or debug.

Huge, nested lambdas are totally at odds with the recommendations of functional design — small composable functions — essentially violating every rule. Likewise, complex template expansions often obscure the meaning of your code and do not actually advance the functional agenda which is rooted in simplicity and clarity.

Functional programming is not about syntax choice, it’s a set of techniques to eliminate side-effects or at least strictly limit their scope. Convenient syntax helps but it isn’t mandatory. Used wisely, functional principles make it easier to write correct and economical code, and that was always the point.

--

--

Rico Mariani
Rico Mariani

Written by Rico Mariani

I’m an Architect at Microsoft; I specialize in software performance engineering and programming tools.