C++ / LAMBDAS AND CALLABLE OBJECTS
Lambda syntax and capture lists
Break any lambda into its five parts and choose the right capture form for every name it borrows from the enclosing scope.
What you will learn
- Parse any lambda as [captures](params) specifiers -> ret { body }
- Pick per name between [x], [&x], [=], [&] and the init-capture [x = expr]
- Hold lambdas in auto, since each lambda expression has its own unnamed class type
- Write -> T when the body's return statements would deduce different types
Understanding Lambda syntax and capture lists
A lambda expression is not a function; it is an expression that builds an object. The compiler reads [captures] (params) specifiers -> ret { body } and synthesizes an unnamed class whose data members come from the capture list and whose operator() carries the parameters, specifiers, return type and body you wrote. That is why a lambda can be copied, stored, and asked for its sizeof: it is a value like any small struct. Only the capture list and the body are mandatory; (params) may be dropped when there are none, and the return type is deduced from the return statements when you omit -> T.
The capture list is the bridge between the enclosing function's automatic variables and the closure object's members. Any name in the body that refers to a local or a parameter of the enclosing function must be reachable through that list, either individually as [x] or [&x], or through a default such as [=] or [&]; the form [name = expr] invents a new member initialized from an arbitrary expression. Names that are not automatic variables of the enclosing function - globals, static locals, functions, types, constants - need no capture at all, which is why a lambda that mentions plenty of names can still have an empty [].
The mental model worth keeping is: read the capture list as the member declarations of a tiny struct and everything after it as that struct's call operator. It explains why two identical-looking lambdas still have different types (two separate structs), why you need auto to hold one, and why a lambda with [] converts to a plain function pointer while one with any capture does not - there is state to carry and a function pointer has nowhere to put it. It also fixes the cost model: the closure is exactly as big as what you listed, so the capture list is a design decision rather than boilerplate.
<iostream>
<string>
<type_traits>
int main() {
int base = 10;
int hits = 0;
// [captures] (params) specifiers -> return_type { body }
auto plain = [] { return 42; }; // no captures, no params
auto add = [base](int x) { return x + base; }; // copy capture
auto tally = [&hits] { ++hits; }; // reference capture
auto tag = [name = std::string("row")](int i) -> std::string {
return name + "-" + std::to_string(i); // init-capture, written return type
};
std::cout << plain() << ' ' << add(5) << '\n';
tally();
tally();
std::cout << "hits=" << hits << '\n';
std::cout << tag(3) << '\n';
int (*fp)() = plain; // only a captureless lambda converts to a function pointer
std::cout << "via pointer: " << fp() << '\n';
std::cout << std::boolalpha
<< "same type: " << std::is_same_v<decltype(plain), decltype(add)> << '\n';
}
A lambda expression declares an unnamed class on the spot: the capture list becomes its data members, and everything after it becomes its call operator.
Worked examples
When the return type must be written
Shows the -> T slot and why deduction cannot handle two returns of different types.
<iostream>
int main() {
auto half = [](int n) -> double {
if (n == 0) return 0; // int literal, converted to double
return n / 2.0; // double
};
std::cout << half(0) << ' ' << half(7) << '\n';
}
Example explained
Line 1-> double sits between the parameter list and the body; that is the only place a lambda's return type can be spelled.
Line 2Without it, return 0; deduces int and return n / 2.0; deduces double, and the lambda is rejected for inconsistent deduced return types.
Line 3With the type declared, return 0; is converted to 0.0, and default stream formatting prints that as 0.
Line 4half(7) yields 3.5 because 2.0 forces floating-point division instead of integer division.
Capture defaults, explicit captures, and when they run
Mixes a copy default with one reference capture and shows that captures are taken where the lambda is created.
<iostream>
int main() {
int width = 4;
int height = 3;
int area = 0;
// copy default for everything the body uses, plus one explicit reference capture
auto compute = [=, &area] { area = width * height; };
width = 100; // changed after compute was built
compute();
std::cout << "area=" << area << '\n';
auto perimeter = [width, height] { return 2 * (width + height); };
std::cout << "perimeter=" << perimeter() << '\n';
}
Example explained
Line 1[=, &area] combines a capture default with one named exception: area is a reference member, width and height are copies.
Line 2Those copies are initialized when the closure object is constructed, so width = 100 afterwards cannot change what compute() sees.
Line 3perimeter is constructed after the assignment, so its own copy of width is 100 and the result is 2 * (100 + 3).
Line 4Writing through the reference member is how a lambda whose body returns nothing still hands a result back to the caller.
Important notes
A capture default captures only the names the body actually uses, so [=] never copies the whole stack frame, but it does hide which objects the closure stores.
The specifier slot between (params) and -> T is where mutable, noexcept and attributes go; init-captures need C++14, and the std::is_same_v in the first sample needs C++17.
Common mistakes
Using an enclosing local in the body without listing it, then making that variable global to silence the error instead of capturing it, which quietly gives it program-wide lifetime and shared state.
Putting in the capture list what should be a parameter: [n] { return n * 2; } can only double the n frozen at construction, while [](int n) { return n * 2; } doubles whatever the caller passes.
Forgetting the semicolon in auto f = [] { ... }; because the closing brace looks like the end of a function definition, which produces an error pointing at the next line instead.
Try it yourself
Change, predict, then run
Declare int lo = 2, hi = 8; and write a lambda fit that captures both by copy, takes an int v, and returns v clamped into [lo, hi] as a double using a trailing return type. Then add an init-capture label = std::string("v=") and print label followed by fit(0), fit(5) and fit(99).
Open the C++ workspaceCheck your understanding
A lambda written with an empty capture list can be assigned to int(*)(int), but adding one capture makes that same assignment fail to compile. What does that tell you about lambdas?
- The capture list becomes data members of the closure object, and a function pointer has nowhere to carry that data
- Captures delay when the body runs, and function pointers cannot express delayed execution
- Function pointers only accept lambdas whose parameters are all built-in types
- A capturing lambda is compiled as a template, so it has no address until it is instantiated
Show answer
The conversion exists precisely because a captureless closure holds no state, so the compiler can emit an ordinary function with the same signature and hand back its address; once the list has entries, the object carries members that a bare pointer cannot transport. The template answer is tempting because generic lambdas do involve templates, but a plain capturing lambda is an ordinary class with an ordinary operator() - what blocks the conversion is stored state, not templates.