Lambda Expressions
Lambdas let you write a small, nameless function right where you use it — turning a one-off comparator or callback that used to require a separate named function or a hand-written functor class into a single inline expression.
Before C++11, passing custom behavior into an algorithm like std::sort meant writing a separate named function, or a small class overloading operator() (a "function object" or "functor"), just to sort by a different key once. A lambda expression collapses that entire ceremony into an inline, anonymous function you write exactly where you need it — std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; }) reads almost like English, and the comparator never needs a name anyone else will ever see. This page covers lambda syntax piece by piece, how captures decide what a lambda can see from its surrounding scope, generic and mutable lambdas, the lifetime traps that come from capturing by reference, and how lambdas fit into the STL algorithms and callback patterns they're used with constantly.
Lambda Syntax
A lambda expression has the general form [captures](parameters) -> returnType { body }. The capture list controls what from the enclosing scope the lambda can use; the parameter list works exactly like an ordinary function's; the return type is usually omitted and deduced automatically from the body's return statement (or void if there is none); and the body is ordinary C++ code.
add's return type is deduced as int from return a + b;, so no -> returnType is needed. divide has an explicit -> double, which is required here because the body has two return statements (return 0.0; and return a / b;) — when a lambda's body would otherwise deduce more than one type, or when you want to be explicit for clarity, you spell out the return type.
The type produced by a lambda expression is a unique, compiler-generated, unnamed class type (technically called a closure type) with operator() overloaded to run the body — every single lambda expression in a program, even two that look textually identical, produces a distinct type. That's exactly why lambdas are almost always stored in an auto variable, or passed directly as a function argument to a template parameter, rather than given an explicit named type.
#include <iostream>
int main()
{
auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 4) << "\n";
auto greet = []() { std::cout << "Hello!\n"; };
greet();
auto divide = [](double a, double b) -> double {
if (b == 0.0) return 0.0;
return a / b;
};
std::cout << divide(10.0, 4.0) << "\n";
return 0;
}Captures: By Value and By Reference
The capture list in [ ] decides which variables from the enclosing scope the lambda body is allowed to use, and how. [x] captures x by value — the lambda gets its own copy, frozen at the moment the lambda is created. [&x] captures x by reference — the lambda refers to the actual variable, seeing any later changes to it (and able to modify it, if the lambda isn't const). [=] captures everything used in the body by value; [&] captures everything used in the body by reference.
byValue captured x's value (10) at the moment the lambda was created, so changing x afterward has no effect on that captured copy — it prints 10. byRef instead stores a reference to x itself, so it reflects x's current value whenever it's called — it prints 99, the value x holds by the time byRef() actually runs.
Mixed captures are common and explicit: [x, &y] captures x by value and y by reference in the same lambda. [=, &y] captures everything by value except y, which is captured by reference; [&, x] captures everything by reference except x, which is by value.
#include <iostream>
int main()
{
int x = 10;
auto byValue = [x]() { std::cout << "by value: " << x << "\n"; };
auto byRef = [&x]() { std::cout << "by ref: " << x << "\n"; };
x = 99;
byValue(); // still sees 10 -- its own frozen copy
byRef(); // sees 99 -- refers to the real x
return 0;
}Generic Lambdas
Since C++14, a lambda's parameters can be declared auto instead of a concrete type, making the lambda generic — its call operator becomes a template, deduced separately for each different set of argument types it's actually called with, exactly like a function template.
print's parameter is const auto&, so it works unchanged for an int, a double, and a std::string — the compiler generates a separate operator() instantiation for each type it's actually called with, just as it would for a function template T print(const T& value). add similarly deduces int + int for its first call and double + double for its second.
#include <iostream>
#include <string>
int main()
{
auto print = [](const auto& value) { std::cout << value << "\n"; };
print(42);
print(3.14);
print(std::string("hello"));
auto add = [](auto a, auto b) { return a + b; };
std::cout << add(2, 3) << "\n"; // int
std::cout << add(2.5, 1.5) << "\n"; // double
return 0;
}Mutable Lambdas
By default, a lambda's operator() is const, which means variables captured by value can be read but not modified inside the body — the lambda's own copy is treated as read-only. Adding the mutable keyword after the parameter list removes that const, allowing the body to modify its by-value captures (though this only changes the lambda's own internal copy; it never affects the original variable back in the enclosing scope).
increment captures counter by value, and mutable lets it increment its own copy each time it's called — because the closure object itself persists between calls (it isn't recreated), that copy keeps accumulating across calls, printing 1, then 2, then 3. The original counter variable back in main is completely untouched, still 0, since it was only ever captured by value.
#include <iostream>
int main()
{
int counter = 0;
auto increment = [counter]() mutable {
++counter;
std::cout << "inside lambda: " << counter << "\n";
};
increment(); // 1
increment(); // 2
increment(); // 3
std::cout << "outside lambda: " << counter << "\n"; // still 0
return 0;
}Lambda Lifetime and Dangling References
A lambda that captures by reference stores an actual reference to the captured variable, not a copy — so if the lambda outlives the variable it referenced, calling it afterward reads or writes memory that no longer holds a valid object, which is undefined behavior. This is the single most common source of subtle bugs with lambdas.
localCount is a local variable of makeDangerousCounter; it's destroyed the instant that function returns. The lambda captured it by reference, so the returned closure is left holding a reference to memory that's no longer valid once main calls counter() — the program's behavior at that point is undefined, whether or not it happens to appear to work.
Capturing localCount by value instead copies it into the closure object at the moment the lambda is created, so the closure carries its own independent copy that lives exactly as long as the closure does — mutable is needed here so that copy can be incremented on each call.
#include <functional>
#include <iostream>
std::function<int()> makeDangerousCounter()
{
int localCount = 0;
return [&localCount]() { return ++localCount; }; // BUG: captures a local by reference
}
int main()
{
auto counter = makeDangerousCounter();
std::cout << counter() << "\n"; // undefined behavior: localCount no longer exists
return 0;
}std::function<int()> makeSafeCounter()
{
int localCount = 0;
return [localCount]() mutable { return ++localCount; }; // captures by VALUE instead
}Lambdas with STL Algorithms and as Callbacks
Lambdas exist largely for exactly this: supplying custom, one-off behavior to a generic algorithm or an API that expects a callback, without the ceremony of a named function or a functor class declared somewhere else in the file.
count_if's lambda captures threshold by value so it can compare each element against it. sort's comparator lambda needs no captures at all, since it only compares its two parameters. for_each's lambda is used purely for its side effect of printing, ignoring its own return type (void here) entirely — for_each calls it once per element, in order.
A std::function<Signature> can hold any callable matching that signature — a lambda, a function pointer, or a functor — which is what makes lambdas usable as general-purpose callbacks stored in a variable, passed around, or kept in a container (a vector of std::function<void()> is a common way to represent a list of pending actions or event handlers).
#include <algorithm>
#include <iostream>
#include <vector>
int main()
{
std::vector<int> nums{5, 3, 8, 1, 9, 2};
int threshold = 4;
int countAbove = std::count_if(nums.begin(), nums.end(),
[threshold](int n) { return n > threshold; });
std::cout << "count above " << threshold << ": " << countAbove << "\n";
std::sort(nums.begin(), nums.end(),
[](int a, int b) { return a > b; });
std::for_each(nums.begin(), nums.end(),
[](int n) { std::cout << n << " "; });
std::cout << "\n";
return 0;
}constexpr Lambdas
Since C++17, a lambda whose body only performs operations legal in a constant expression is implicitly constexpr, and can be marked constexpr explicitly for clarity or to force a compile error if it turns out not to qualify. A constexpr lambda can be evaluated at compile time when used in a constant-expression context.
square is marked constexpr, and square(5) with a compile-time-known argument is folded into the constant 25 by the compiler with no runtime multiplication at all. The same lambda still works perfectly well at runtime too — square(runtimeValue) computes 49 during program execution, since runtimeValue isn't known until then.
#include <iostream>
int main()
{
constexpr auto square = [](int n) { return n * n; };
constexpr int result = square(5); // evaluated at compile time
std::cout << result << "\n";
int runtimeValue = 7;
std::cout << square(runtimeValue) << "\n"; // this call happens at runtime
return 0;
}Best Practices
Capture only what you actually use, and name captures explicitly ([x, &y]) rather than reaching for [=] or [&] by default — it documents intent and prevents accidental dangling references or unnecessary copies. Never let a lambda that captures by reference outlive the variables it references; if a lambda might be called later or from elsewhere, capture by value instead. Prefer auto for storing a lambda locally, and std::function only when you genuinely need type erasure (storing different callables with the same signature in one variable or container), since std::function carries extra overhead a plain lambda-typed auto variable doesn't. And keep lambda bodies short — a lambda that's grown into a multi-page block of logic is a sign it should become a named function instead, for the same readability reasons that apply to any other function.
What is the difference between capturing a variable by value ([x]) and by reference ([&x]) in a lambda, and what problem can capturing by reference cause?
Capturing by value copies the variable's current value into the lambda's closure at the moment the lambda is created, so later changes to the original variable don't affect the lambda's own copy. Capturing by reference stores an actual reference to the original variable, so the lambda always sees its current value — but if the lambda is called after the referenced variable has gone out of scope and been destroyed (for example, a local variable in a function that already returned), that reference is dangling and using it is undefined behavior.
Why does a lambda need the mutable keyword to modify a variable it captured by value, and does modifying it inside the lambda affect the original variable?
A lambda's operator() is const by default, so by-value captures are treated as read-only members of the closure object; mutable removes that const qualifier so the body can modify its own copy. That modification only affects the closure's internal copy — the original variable back in the enclosing scope, which was only copied at capture time, is completely unaffected.
What makes a lambda 'generic,' and how does the compiler handle a call like add(2, 3) followed by add(2.5, 1.5) on the same generic lambda auto add = [](auto a, auto b) { return a + b; };?
A generic lambda uses auto for one or more parameters, which makes its call operator a template rather than an ordinary function, deduced separately for each distinct combination of argument types it's called with. add(2, 3) instantiates and calls a version specialized for int, int, while add(2.5, 1.5) instantiates a separate version specialized for double, double — exactly as if add were a function template.
Why is it dangerous for a function to return a std::function that captures one of that function's own local variables by reference?
The local variable is destroyed the instant the function returns, but a by-reference capture only stores a reference to it, not a copy — so the returned closure ends up holding a reference to memory that's no longer valid. Calling that closure afterward reads or writes through a dangling reference, which is undefined behavior; the fix is to capture such variables by value instead, so the closure carries its own independent copy that lives as long as the closure does.
Why is every lambda expression's type considered unique and unnamed by the compiler, and what's the practical consequence for how lambdas are typically stored?
Each lambda expression compiles to its own distinct, compiler-generated closure class with a unique operator() implementing that lambda's body — even two lambdas with textually identical bodies produce two different types. In practice this means you almost always store a lambda in an auto variable (letting the compiler track its exact type for you) or pass it directly as an argument to a function template parameter, rather than trying to name its type explicitly; when you do need a uniform named type across different callables, std::function<Signature> is used instead.
What does this code print, and why doesn't changing base afterward affect the result?
It prints 15. addBase captured base by value, so it stored a copy of base's value (10) at the moment the lambda was created; the later assignment base = 100 changes the original variable but has no effect on the lambda's already-captured copy. addBase(5) computes 5 + 10 = 15, not 5 + 100.
Write a C++ program that uses std::sort with a lambda comparator to sort a std::vector<std::pair<std::string,int>> of (name, score) pairs by score in descending order.
The lambda compares only the .second member of each pair (the score), and returning a.second > b.second orders the pairs from highest score to lowest. The result puts Bob and Dave (both 92) first, followed by Alice (85) and Carol (78) — std::sort is not guaranteed stable, so Bob and Dave's relative order between themselves isn't guaranteed, though the rest of the ordering by score is correct.
Write a C++ program using a lambda with mutable that acts as a running-total accumulator: each call adds its argument to an internal total (captured by value) and returns the new total.
The capture [total = 0] is an init-capture (C++14) that creates a new variable total inside the closure, initialized to 0, rather than capturing an existing outer variable — this is a common way to give a lambda its own persistent state without needing an outer variable at all. mutable lets each call update that internal total and it persists across calls on the same closure object, producing the running totals 5, 15, 18.
Write a C++ program that stores three different lambdas with the same signature (int, int) -> int (add, subtract, multiply) in a std::vector<std::function<int(int,int)>> and calls each one on the same pair of numbers.
std::function<int(int,int)> erases the specific closure type of each lambda, letting three otherwise-incompatible lambda types be stored side by side in the same vector as long as they share the call signature. Calling op(x, y) for each stored function in turn prints 13 (10+3), 7 (10-3), and 30 (10*3).
int base = 10;
auto addBase = [base](int x) { return x + base; };
base = 100;
std::cout << addBase(5) << "\n";