Capture Lists in Lambda Expressions
A lambda expression is a notation for generating a function object, and it does so in the same way as an expression such as Less_than<int>{x}.
Try it: Functions as Values: Lambda
That a lambda (like def) evaluates to a function object that a name can refer to and that can be passed to other functions — apply(f, x), twice(f, x), map(f, xs), sorted(xs, key=f) — and that each call is traced by binding the argument, substituting it into the single expression and evaluating in operator order.
How it works
- lambda x: expression evaluates to a function object (named <lambda>); nothing inside it is computed yet. f = ... gives it a name, exactly as def f(x): return expression would.
- Writing f without parentheses passes the function object itself; inside apply or twice the parameter func refers to the same object.
- A call binds the argument to the parameter x, substitutes it into the expression, and evaluates by operator precedence (* and % before + and -, parentheses first).
- The value of the expression is the call's return value; there is no other body.
- map(f, xs) calls f once per item and keeps the results in order; sorted(xs, key=f) calls f once per item to get keys, orders by the keys (ties keep their order) and returns the items.
Default run (9 steps): The program is ready: a lambda written inline, then f(x). … print shows 7.
Simplified: Four one-parameter lambdas with editable integer constants, small int arguments (at most 6 items). Each substituted expression the lab shows was evaluated by CPython 3.12 and has the same value as the real call; printed results and ZeroDivisionError messages match CPython.
Loading the simulation…
A Function Written Exactly Where It Is Needed
Most C++ code you have met so far names its operations. You write a function, give it a name, and call it by that name wherever you need it. A lambda expression works differently. It lets you write an operation inline, at the exact place where it is needed, without giving it a name first. The notation can look dense at a glance, with square brackets, parentheses, and a body all packed together, but it follows a small set of rules. This article builds those rules step by step.
Many useful functions are small and short-lived. You write one to hand to an algorithm, filter a collection, or sort some items, and then you never need it again. Writing a full named function for each of those jobs is heavy. A lambda expression lets you write the function right where it is needed. The harder question is what such a small function is allowed to see. Its body may refer to names that were declared in the surrounding code, and the capture list is the part of the lambda that answers how those outside names are reached.
Keep one question in mind as you read: when the lambda's body mentions a local name from the place where the lambda was written, does the function work with that name itself, or with a separate value it has been given? The rest of this article answers that question, first by tracing what a lambda actually produces, and then by reading the notation that decides the answer.
Anonymous Function or Function Object
A common first reaction is to treat a lambda as an anonymous function: a function that happens to have no name, but is otherwise the same kind of thing as a named function. Under that picture, a lambda is simply a shortcut for writing a function somewhere else in the program.
This picture is close enough to get started, but it misses the part that makes lambdas useful in practice. A lambda expression is a notation for generating a function object. The thing it produces is an object that can be used like a function, and the capture list in its brackets decides how that object gets access to names from the surrounding code. Keep this distinction in mind, because the rest of the article depends on it.
A function object is an object that can be used like a function. Writing Less_than<int>{x} creates such an object from a named type and an argument. A lambda expression produces an object of the same general kind, but you never have to name its type or write a separate class for it.
The source uses the notation [&](int a){ return a<x; } as its running example. It calls this whole expression a lambda expression, and it states that the notation generates a function object in the same way that the expression Less_than<int>{x} does. So the lambda is not the object itself. It is the notation that tells the compiler how to build one. The object it builds is a function object constructed with x as its stored value, and it can be called with an int to return the result of a comparison.
The source explains why this matters. Function objects let you separate the traversal of a container from the operation performed on each element. The container decides how to walk through its elements, and the object decides what to do with each one. A lambda gives you that separation without the bookkeeping of naming a type first.
Three Parts of the Notation
Every lambda expression in this article has the same three parts, written in a fixed order. The first is the capture list, inside square brackets. The second is the parameter list, inside parentheses, which names the values the generated object receives when it is called. The third is the body, inside braces, which contains the operation itself.
The capture list is the one part this article focuses on. The parameter list and body work the way you already expect from a function: the parameter a receives the value the object is called with, and the body computes a result from it. What the capture list adds is the answer to a question the parameter list cannot answer, namely how the body reaches x, a name declared outside the lambda.
The capture list always sits first, inside square brackets, before the parameter list. A bare ampersand or equals sign applies to every local name the body uses. An ampersand or equals sign followed by a name applies only to that one name. The ampersand means the body works through a reference to the original. The equals sign means the generated object receives a copy of the value. Empty brackets mean the body is given no local names at all.
Tracing One Lambda Call
Consider the source's snippet. The name less_than_x holds the function object that the lambda generated. Calling it with 5 runs the body a < x using the parameter a, so the object compares 5 against the x it has access to. The capture list [&] is what gives the body access to x, which is declared outside the lambda.
Now trace what happens when the outer x changes after the lambda has been created. The source compares two versions of the same program. If the lambda had captured x by reference, the same call would see the current value of x, which is 10, and the comparison 7 less than 10 would be true. The two programs differ only in the capture list, and that one part changes the result.
Notice what the reference capture is doing. It does not hold its own number. It sees the current state of the original name, so every change to x is visible to the body the next time the object is called. The value capture works the other way. The object keeps a copy, separate from the original, and later changes to x do not reach it.
What the Capture List Decides
The capture list is the part in square brackets that determines how local names used in the body are accessed. The forms you will meet are the empty brackets, the bare ampersand, the ampersand with a name, the bare equals sign, and the equals sign with a name. Each form makes a different set of outside names available, and each form accesses them in one of two ways.
The two access modes are the core of the rule. A reference capture, written with an ampersand, lets the body work through a reference to the original name. A value capture, written with an equals sign, gives the generated object a copy of the value. The syntax is identical apart from that one character, so reading the capture list carefully is the habit to build. Whenever you see an ampersand, ask which outside name the body is actually using, and whether that name can change between the moment the lambda is written and the moment it is called.
| Capture list | Which outside locals are available | How they are accessed |
|---|---|---|
| [ ] | None | Not applicable, since no outside locals are used |
| [&] | Every outside local the body uses | By reference |
| [&x] | Only x | By reference |
| [=] | Every outside local the body uses | By value (the object holds a copy) |
| [=x] | Only x | By value (the object holds a copy) |
Summary of the capture list forms covered by the source
Capture-nothing with [ ] is the right choice when the body needs no outside local names. Capture-all-by-reference with [&] and capture-all-by-value with [=] apply to every name the body uses. Capturing a single named local with [&x] or [=x] limits the lambda to exactly the name you name.
Reading Two Capture Lists Side by Side
Suppose a program has a local variable x and wants two function objects that compare an incoming int against it. One lambda should refer to x directly, and the other should carry its own copy. The two lambdas look nearly identical, so the capture list is the only place where their behavior is decided. Read the two versions below and ask, for each one, which x the body will use.
0 1- x starts at 3, and both lambdas are created while x is 3
- [=] gives
by_valueits own copy of x, which holds 3 - [&] gives
by_referencea reference to the original x - x is changed to 10 after both lambdas exist
by_value(7) compares 7 with the copy 3, so 7 < 3 is false and prints 0by_reference(7) compares 7 with the current x, 10, so 7 < 10 is true and prints 1
What do you think happens?
Before reading the output, predict: after x is changed to 10, what does by_value(7) return, and what does by_reference(7) return?
Reveal answer
by_value returns false and by_reference returns true, so the program prints 0 1.
The value capture holds the copy 3 made when the lambda was created, so 7 is not less than 3. The reference capture reads the original x, which is now 10, so 7 is less than 10.
Mistakes Beginners Make with Capture Lists
Common mistake Treating a lambda as a plain anonymous function with nothing else going on
Why it failsA lambda is a notation for generating a function object. Its capture list decides how that object reaches outside names, so ignoring the object misses the part that determines results.
Do this insteadRead every lambda as producing an object that stores whatever its capture list gathered, and ask what that object holds.
Common mistake Assuming [&] and [=] give the same result because the bodies look identical
What it looks likeWith x set to 3, then changed to 10, by_value(7) returns false while by_reference(7) returns true.
Why it failsThe access mode is the whole difference. A reference sees later changes to x, while a value capture keeps the copy made at creation.
Do this insteadDecide whether the body should see the current state of the original name or a snapshot, and choose & or = to match.
Common mistake Using [ ] when the body refers to an outside local
Why it failsEmpty brackets capture nothing, so the body has no access to outside local names.
Do this insteadEither list the name in the capture list, such as [&x] or [=x], or use [&] or [=] when the body uses several outside names.
Common mistake Assuming the parameter list can supply an outside value
Why it failsThe parameter list names the values passed in when the object is called. It does not make outside locals visible to the body.
Do this insteadPut the outside name in the capture list, and keep the parameter list for values that arrive at each call.
Lambda or Named Operation
Lambdas are convenient, and the source does not present them as a replacement for named code. For a short expression, an inline lambda keeps the operation next to the call that uses it, so a reader can see the logic in the same place as the code that depends on it. For nontrivial actions, which the source describes as more than a simple expression, the source prefers to name the operation. A name states the purpose of the code, and it makes the operation available in several places in the program.
Use an inline lambda when the operation is short, used in one place, and clearer next to the call. Choose a named operation when the action is nontrivial, when its purpose benefits from a name, or when several parts of the program need it.
Practice Reading Capture Lists
A local variable count is set to 4 when the lambda is created, and it is changed to 9 afterward. For each of these capture lists, state whether the body sees 4 or 9 when the object is called: [=count], [&count], and [ ]. For the empty list, say what the body is allowed to refer to.
Hints
- Ask whether the capture list gives the object a copy or a reference.
- A value capture keeps the value present when the lambda was created.
- The empty list captures no local names at all, so the body cannot use count.
Explain, in your own words, why a lambda is better described as a notation for generating a function object than as an anonymous function. Then give one situation where you would prefer a named operation over an inline lambda, and say why.
Hints
- Think about what the object stores and where the captured names live.
- Consider how often the operation is needed and whether its purpose benefits from a name.
Key Takeaways
- A lambda expression is a notation for generating a function object, similar in kind to writing Less_than<int>{x}, and the generated object can be used like a function.
- A lambda has three parts: the capture list in square brackets, the parameter list in parentheses, and the body in braces.
- [ ] captures nothing, [&] and [&x] access outside locals by reference, and [=] and [=x] give the object a copy of the value.
- A reference capture sees later changes to the original name, while a value capture keeps the copy it made and is not affected by later changes.
- Use an inline lambda for a short operation placed next to its call, and prefer a named operation for nontrivial actions or ones needed in several places.