Lesson 1 of 5
You have been writing lambdas since Module 13 without them being explained:
std::count_if(v.begin(), v.end(), [](int x) { return x > 5; });
That [](int x) { return x > 5; } is a lambda — a function defined at the point of use, with no
name.
The anatomy
[captures](parameters) -> return_type { body }
^^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^ ^^^^
required optional optional required
Only the capture list and the body are required. The smallest legal lambda is []{}.
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<int> v = {5, 2, 8, 1, 9};
// The anatomy: [captures](parameters) -> return_type { body }
auto is_big = [](int x) { return x > 4; };
std::cout << "count > 4: " << std::count_if(v.begin(), v.end(), is_big) << "\n";
// Return type is deduced; write it explicitly when the deduction is wrong.
auto half = [](int x) -> double { return x / 2.0; };
std::cout << "half of 5: " << half(5) << "\n";
// Called immediately — useful for initialising a const from several steps.
const int total = [&v] {
int sum = 0;
for (int x : v) sum += x;
return sum;
}();
std::cout << "total: " << total << "\n";
// A lambda is an object with a type only the compiler can name.
std::cout << "sizeof empty lambda: " << sizeof(is_big) << "\n";
int threshold = 4;
auto captures_one = [threshold](int x) { return x > threshold; };
std::cout << "sizeof capturing one int: " << sizeof(captures_one) << "\n";
// Generic lambda (C++14): auto parameters make it a template.
auto print = [](const auto& value) { std::cout << value << " "; };
print(42);
print(3.5);
print(std::string("text"));
std::cout << "\n";
return 0;
}
count > 4: 3
half of 5: 2.5
total: 25
sizeof empty lambda: 1
sizeof capturing one int: 4
42 3.5 text
Four things there are worth naming.
The return type is deduced from the return statement. half needs the explicit -> double
because without it the deduction is still double — but write it when you want to be sure, or when
there are several return statements that must agree.
A lambda called immediately — [&v] { ... }() — lets you initialise a const from a multi-step
computation. Without it you would need a non-const variable, or a named function. This is the
immediately-invoked lambda, and it is the cleanest way to make something const that takes work to
compute.
sizeof an empty lambda is 1, not 0, because every object must have a distinct address. Capture
one int and it becomes 4. A lambda's size is the size of what it captures — which is the clue
to what it really is.
A generic lambda (C++14) uses auto parameters. print accepts an int, a double, and a
std::string, because auto parameters make operator() a template (Module 12).
What it compiles to
A lambda is not a special language construct at run time. It is a struct with operator():
#include <iostream>
#include <string>
// What a lambda actually compiles to: an unnamed struct with operator().
struct IsAboveEquivalent {
int threshold; // the capture, as a member
bool operator()(int x) const { return x > threshold; }
};
int main() {
int threshold = 4;
auto lambda = [threshold](int x) { return x > threshold; };
IsAboveEquivalent functor{threshold};
std::cout << std::boolalpha;
std::cout << "lambda(5): " << lambda(5) << "\n";
std::cout << "functor(5): " << functor(5) << "\n";
std::cout << "same size: " << (sizeof(lambda) == sizeof(functor)) << "\n";
// The lambda's operator() is const by default, so this would not compile:
// auto counter = [threshold]() { ++threshold; };
// unless marked mutable:
auto counter = [n = 0]() mutable { return ++n; };
std::cout << counter() << " " << counter() << " " << counter() << "\n";
// mutable modifies the lambda's OWN copy. The original is untouched.
int outside = 100;
auto by_value = [outside]() mutable { outside += 1; return outside; };
std::cout << "inside: " << by_value() << ", outside still: " << outside << "\n";
return 0;
}
lambda(5): true
functor(5): true
same size: true
1 2 3
inside: 101, outside still: 100
The lambda and the hand-written struct behave identically and are the same size. That is because the compiler writes that struct for you — the standard calls it a closure type.
Everything else about lambdas follows from this one fact:
- Captures are members. That is why
sizeofgrows with them. operator()isconstby default, so a by-value capture cannot be modified withoutmutable.- The type has no name you can write. Only
auto(or a template parameter, orstd::function) can hold it. - Every lambda has a distinct type, even two with identical text.
decltype(a) == decltype(b)is false.
mutable
By default the generated operator() is const, so a by-value capture is read-only. mutable
removes the const:
// auto counter = [n = 0]() mutable { return ++n; };
counter now has state that persists between calls — a stateful callable. The output 1 2 3
shows it counting.
But read the last two lines of the output again. by_value() returned 101 while outside stayed
100. mutable lets the lambda modify its own copy, not the original. If you wanted the original
changed, you needed a reference capture, which is lesson 14.2.
That is also why a mutable lambda passed to an algorithm is a trap: the algorithm may copy it
(lesson 13.8), and each copy has its own counter.
Where the -> type is needed
Return type deduction usually works. It fails in one common case:
// auto bad = [](bool flag) {
// if (flag) return 1; // deduces int
// else return 2.5; // deduces double — error, inconsistent
// };
//
// auto good = [](bool flag) -> double {
// if (flag) return 1; // converted to double
// else return 2.5;
// };
The other case is when you want a reference return, since deduction strips it:
// auto by_value = [](std::vector<int>& v) { return v[0]; }; // returns int
// auto by_ref = [](std::vector<int>& v) -> int& { return v[0]; }; // returns int&
Lambdas in the wild
The three shapes you will write most:
// A predicate for an algorithm — the most common by far.
// std::sort(v.begin(), v.end(), [](const auto& a, const auto& b) {
// return a.score > b.score;
// });
//
// A small transformation.
// std::transform(in.begin(), in.end(), out.begin(),
// [](int x) { return x * x; });
//
// A callback stored for later — needs std::function or a template (lesson 14.4).
// button.on_click([this] { this->save(); });
When not to use one
When it needs a name. A lambda assigned to a variable and used in three places wants to be a named function. Lambdas earn their keep by being local — defined where used, once.
When it is long. More than about five lines inside an algorithm call and the call site becomes unreadable. Extract a named function.
When you need it before it is defined. A lambda cannot call itself by name (there is no name).
Recursion needs a named function, or a std::function, or C++23's deducing this.
Try it
Write lambdas for: squaring a number, checking if a string is longer than n characters, comparing
two records by a chosen field, and a counter that returns 1, 2, 3 on successive calls. Then
determine sizeof each and explain the differences.
Checkpoint
- A lambda is
[captures](params) -> ret { body }; only the captures and body are required. - It compiles to an unnamed struct with
operator()— the closure type. - Captures are members, which is why
sizeofgrows with them; an empty lambda is 1 byte. operator()isconstby default;mutableremoves that.mutablemodifies the lambda's own copy, never the original variable.- Every lambda has a distinct type, even two with identical text.
- The type has no spelling — hold it with
auto, a template parameter, orstd::function. - Write
-> typewhen severalreturns disagree, or when you need a reference return. - An immediately-invoked lambda
[]{ ... }()initialises aconstfrom a multi-step computation. autoparameters (C++14) make a generic lambda —operator()becomes a template.- Extract a named function when the lambda needs a name, exceeds a few lines, or must recurse.
Questions about this lesson
Log in to ask a question.