Concepts / Capture Lists in Lambda Expressions

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}.

  • Programming
Interactive lab

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

  1. 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.
  2. Writing f without parentheses passes the function object itself; inside apply or twice the parameter func refers to the same object.
  3. A call binds the argument to the parameter x, substitutes it into the expression, and evaluates by operator precedence (* and % before + and -, parentheses first).
  4. The value of the expression is the call's return value; there is no other body.
  5. 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.

Educational simulation

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.

Definition

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.

Lambda expressionnotation written in thecodeFunction objectgenerated by the notationCaptured namesx, held by reference or byvalueCalled like afunctionless_than_x(5)generatesstoresis used as
The lambda expression is notation. It generates a function object, and that object stores the captured names and can be called like a function.

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.

[&]capture list: which outsidenames are reached, and how(int a)parameter list: the valuepassed in when called{ return a<x; }body: the operationperformed on athenthen
Each token of the lambda expression contributes one part: the brackets hold the capture list, the parentheses hold the parameters, and the braces hold the body.

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.

Outer x3Outer x10Value-captured copy3Value-captured copy3Reference-captured x10set to 10no change
Before the call, the outer x is 3 and the value-captured copy is 3. Then x is changed to 10. The reference capture sees 10, while the value-captured copy stays at 3.

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.

[&]all outside locals the bodyuses[=]all outside locals the bodyuses[ ]nothing is captured[&x]only x[=x]only x
Each capture list form gives the lambda access to either all outside locals it uses or only the one named, and the access is either by reference or by value. The empty form gives no outside locals at all.

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 listWhich outside locals are availableHow they are accessed
[ ]NoneNot applicable, since no outside locals are used
[&]Every outside local the body usesBy reference
[&x]Only xBy reference
[=]Every outside local the body usesBy value (the object holds a copy)
[=x]Only xBy value (the object holds a copy)

Summary of the capture list forms covered by the source

Key point

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.

cpp
Output
0 1
Notes on the code
  • x starts at 3, and both lambdas are created while x is 3
  • [=] gives by_value its own copy of x, which holds 3
  • [&] gives by_reference a 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 0
  • by_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?

  • Both return true

  • by_value returns false and by_reference returns true

  • by_value returns true and by_reference returns false

  • Both return false

Reveal answer
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 fails

    A 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 instead

    Read 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 like

    With x set to 3, then changed to 10, by_value(7) returns false while by_reference(7) returns true.

    Why it fails

    The 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 instead

    Decide 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 fails

    Empty brackets capture nothing, so the body has no access to outside local names.

    Do this instead

    Either 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 fails

    The parameter list names the values passed in when the object is called. It does not make outside locals visible to the body.

    Do this instead

    Put 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.

Inline lambdalogic visible at the callsiteNamed operationlogic stated once, under anameShort expressionbest fitReused in severalplacesbest fit
An inline lambda keeps its logic at the call site and is used once. A named operation places its logic in one place and can be reused from several places.
Best practice

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

EASY

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.
MEDIUM

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.

Related concepts

More concepts starting with C