Lambdas & Functional C++
What [](){} actually compiles to, and how it plugs into the STL algorithm library.
Intermediate
[ capture ] ( parameters ) -> return_type { body }
auto square = [](int x) { return x * x; }; // return type deduced automatically
square(5); // 25
[]{}, is equivalent to []() -> void {}
— every part except the capture clause and body is optional. If a lambda has multiple
return statements, they must all deduce the same type, or you write the return
type explicitly.
| Capture | Meaning |
|---|---|
[&] | everything from the enclosing scope, by reference |
[=] | everything from the enclosing scope, by value (copied) |
[a, &b] | a by value, b by reference — mix explicitly per variable |
[&, a] | everything by reference, except a by value |
[=] is convenient but worth using deliberately rather than as a default
— it silently copies everything the body touches, which is easy to miss when that
includes a large collection you didn't mean to duplicate. [&] avoids the
copy but has the opposite risk: if the lambda outlives the variables it references (stored
and called later, say, after the enclosing function has returned), those references dangle.
operator() holding the body. Two lambdas with byte-identical
source text, written in the same scope, are still two different types — each
[]{...} expression mints its own class.
auto lb = [](int x) { return 5 * x; };
// roughly equivalent, from the compiler's perspective, to:
class __CompilerGeneratedClosure {
public:
auto operator()(int x) const { return 5 * x; }
};
lb(5); // really __CompilerGeneratedClosure_instance.operator()(5)
auto l1 = []{}; auto l2 = []{};
std::is_same<decltype(l1), decltype(l2)>::value; // false — same source, different generated types
auto l3 = l1; // fine — l3 is a copy, same type as l1
const
operator() — the lambda can't mutate its own copies. The mutable
keyword lifts that restriction for the lambda's internal copy specifically; it still doesn't
affect the original variable back in the enclosing scope.
int counter = 0;
auto inc = [counter]() mutable { return ++counter; }; // mutates the LAMBDA's own copy of counter
inc(); inc(); // returns 1, then 2
std::cout << counter; // still 0 — the outer counter was never touched
map/filter/reduce
functions the way Python does — the same three operations are
std::transform, std::copy_if, and std::accumulate,
each taking a lambda as the operation to apply:
std::vector<int> nums{10, 11, 12, 13, 14, 15};
std::vector<int> tripled, odds;
std::transform(nums.begin(), nums.end(), std::back_inserter(tripled),
[](int v) { return v * 3; }); // map
std::copy_if(tripled.begin(), tripled.end(), std::back_inserter(odds),
[](int v) { return v % 2 != 0; }); // filter
int total = std::accumulate(odds.begin(), odds.end(), 0,
[](int acc, int v) { return acc + v; }); // reduce
std::find_if, std::count_if,
std::sort with a comparator lambda, and more all follow the same "algorithm plus
a small predicate" shape.
mutable lambda's captured copy
diverging from the original variable, run end to end:
TopNotchNote/cpp/lambda_map_filter_reduce.cpp
std::function<Signature> is a type-erased wrapper that can hold any
callable matching that signature — a lambda, a function pointer, or a bind expression
— which is what lets you store a callback in a member variable or a container, where a
lambda's own unique, unnameable closure type wouldn't fit.
std::function<int(int, int)> op = [](int a, int b) { return a + b; };
op(2, 3); // 5 — op can later be reassigned to a totally different callable with the same signature
std::bind partially applies a function — fixing some arguments and
optionally reordering the rest — producing a new callable. It predates lambdas being
fully general and is largely superseded by a lambda that does the same thing more readably,
but it still shows up often enough in existing code to recognize:
bool atLeast(int x, int y) { return x >= y; }
auto over21 = std::bind(atLeast, std::placeholders::_1, 21); // fixes the second argument to 21
std::count_if(ages.begin(), ages.end(), over21);
// the lambda equivalent, generally preferred in new code:
std::count_if(ages.begin(), ages.end(), [](int x) { return x >= 21; });
constexpr functions (see constexpr &
static) usually accomplish the same compile-time computation far more readably than the
classic template-recursion tricks did.