At a glance:
C++11 lambdas are a topic that I see regularly confuse developers. Their strange syntax can be off-putting to someone who hasn’t come across them before. Those who use them regularly can also be unsure about how they really work — are they expensive? Can I copy them around? Will they bloat my code-base? Unlike friendship however, lambdas are not magic: they’re a simplified way to write a specific type of class in C++ known as a functor.
In this article I aim to demystify what a lambda really is under the hood, but note that this is not a beginner’s guide to lambdas. I’m assuming you have used C++ lambdas before in some capacity, and that you understand the basics of C++ classes and template programming.
- Lambdas are just regular C++ classes
- They make use of the operator() member function
- These are actually called “functors” in C++
- There is no extra overhead or performance penalty for using them
This is first in a two-parter about lambdas, please read the second half here afterwards:
Here’s a basic use of a lambda. Given a list of people, we want to filter out only those who are old enough to be considered adults. Thanks to the fact we can write a lambda inline, it’s a very terse and simple piece of code.
Note how in the lambda declaration, we capture the variable cutoffAge
, accept a parameter of type const Person&
, and return a boolean (despite never explicitly defining a return type.)
Before we dive into looking at lambdas, first let’s discuss some concepts that will naturally lead us towards lambdas.
At some point in their career, all programmers need to sort a list of objects in a non-trivial manner. Let’s say your boss has just given you an important task: We have a list of students, and need to sort them alphabetically! It truly is a task for the ages.
In order to perform a sort, we need a comparator function that lets us know which student is “greater than” the other, as such:
We also need a sort function that can accept this comparator. Here is a bare-bones example. I’ve not actually written any sorting logic, as that would be unnecessary noise.
Note how we’ve used a template which will let us pass anything we want into the second argument — in this case, we’ll pass a function. We can then write our sort however we wish, and use this comparator
function when required.
If the idea of passing a function to another function seems novel to you, don’t be alarmed — this is a core part of functional programming languages like Haskell and a lot of JavaScript.
Calling studentSort
is simple:
Note how we’ve literally just typed the name of the function we want to pass through to studentSort
without any fancy syntax. We’ve now got a way to pass functions to other functions. This is an incredibly powerful concept as it is, but we can go further from here.
In C++, functors are a fancy word for “classes that can act like functions”. A key advantage of this is we can provide state to the functor before the function runs — we’ll come back to this in a bit.
First, let’s convert isFirstStudentGreaterThanSecond
to a functor called StudentComparator
. We’ll have to update our studentSort
function to call the member function of our new functor class.
Our comparator is now a class with a .compare function on it. In order to use this, we’ll need to instantiate it.
We’re almost done with making this a “proper” functor. I said they’re “classes that act like functions”, but right now it looks like a regular class. Enter operator()
.
We can make the following tweak to our StudentComparator
and make it read like a regular function as so:
A new problem has suddenly appeared from your boss: Mrs Miggins, the sweet old dear who entered all the students’ names into the system, forgot to capitalise some of them. Your sort is case-sensitive, meaning all the names that weren’t capitalised have gone to the end of the list!
Let’s not simply make the sort case-insensitive; let’s add the ability for users of our sort decide what they want. We can do this by adding an isCaseSensitive
boolean to our StudentComparator
class, and passing in the value in the constructor.
We can now modify how our pre-set function will behave, by passing in configuration values to the constructor. Rather than writing a separate comparator for case-sensitive and case-insensitive sorts, we can augment an existing one.
StudentComparator
is starting to look like quite a lot of code for a simple concept. In this case it’s fine, but what if we needed to create a lot of arbitrary sorters? Or other functors, such as for transforming or filtering lists?
Let’s condense all we’ve learnt in the past few paragraphs into a few simple lines:
That’s right: a lambda is syntactic sugar for declaring a functor, where a functor is a class that overloads operator()
. The capture list of a lambda refers to variables that will be passed into a constructor.
Note that we’ve used auto
for the type, as the type will actually be generated at compile-time, and as such we can’t know what it is. We also haven’t mentioned the return type in the lambda — this is automatically inferred in this case to be bool
.
Yep. While there are further rules about how to declare lambdas and how capturing works in certain scenarios, fundamentally a lambda compiles to no more than a simple class that overloads operator()
and may or may not “capture” variables through its constructor.
No. 99% of the code generated by your compiler is identical between manually writing out a functor class, and writing a lambda. (We’ll look at this more in part 2.)
A C++ functor is very different to the functional programming definition of a functor. While I won’t talk about the difference here, know that they are not the same thing and could lead to a lot of confusion if you conflate them.
Passing in these variables in this manner to a functor is known as “capturing” them. In other languages like JavaScript and Rust, it is known as “closing over” the variables. If you’ve heard the word “closure” before, the variable comparator
is a closure. Note that in this context, a functor is not necessarily a closure, but a closure is a functor.
A lambda itself is not a functor or closure — it is an expression which creates one. You can think of a lambda like a class, and the functor as the actual instantiated object.
Hopefully I’ve demystified what’s going on behind the scenes with lambdas.
From here, you can already make more informed decisions about things like when it is safe to pass a lambda around, and whether it will allocate on the stack or the heap. (It’s on the stack!)
There’s extra nuance to lambdas however than what I’ve described here. In part 2, we’ll take a brief look at the assembler output, consider what happens when you copy a lambda, and discover why by default you can’t mutate variables captured by value.