Concepts / Lambda Expressions as Arguments to Algorithms

Lambda Expressions as Arguments to Algorithms

A lambda expression is a notation that generates a function object, similar in kind to writing 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 Small Function Written Where It Is Needed

Many useful functions are small and short-lived. You write one to hand to an algorithm, to filter a collection, or to 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. 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 capture list is the part of the lambda that answers this question, and the rest of this article is about reading it.

What a Lambda Expression Produces

Definition

A lambda expression is a notation that generates a function object. It is similar in kind to writing Less_than<int>{x}, which is a function object constructed with x as its stored value. So a lambda is not a special kind of call. It is a compact way of producing an object that can be called with an int and that returns the result of a comparison.

cpp
Output
true
Notes on the code
  • The square brackets [&] form the capture list, which comes first.
  • The parenthesized (int a) is the parameter list the object is called with.
  • The name less_than_x holds the function object that the lambda generated.
  • Calling less_than_x(5) runs the body a < x using the parameter a set to 5.

The call less_than_x(5) returns true because 5 is less than the outer x, which is 10. The body reaches x even though x was declared outside the lambda, and the capture list is what makes that possible.

Worked example

Following one call to a lambda's function object

Given int x = 10 and auto less_than_x = [&](int a){ return a < x; };, what does less_than_x(7) return?

  1. 1
    Identify the stored object

    less_than_x is the function object the lambda generated. Calling it runs the lambda body.

  2. 2
    Bind the parameter

    The parameter a receives the argument 7 for this call.

  3. 3
    Reach the outer name

    The body mentions x. The capture list [&] lets the body reach the outer x by reference, so its current value 10 is used.

  4. 4
    Evaluate the comparison

    The body computes 7 < 10, which is true.

Result

true

Reading the Capture List

A capture list always sits first, inside square brackets, before the parameter list. Once you see the pattern, the rule is simple. 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.

[opens the capture list&reference accessxthe one named local]closes the list, parametersfollow
In a capture list written as [&x], which part sets the access mode and which part names the local?

Read the capture list from left to right. The first symbol tells you the access mode, and the name after it tells you which local the rule covers. A symbol with no name covers every local the body uses. The table below summarizes the forms the source describes.

Capture listHow the body reaches the names it uses
[&] or [&x]Through a reference to the original local
[=] or [=x]Through a copy of the value, held by the generated object
[ ]No local names are captured, so the body cannot use outside locals

Capture list forms and their access modes

Reference Versus Copy in Practice

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. The difference matters whenever the surrounding code's value of x and the object's view of x could diverge.

cpp
Output
by_ref(15): true
by_val(15): false
Notes on the code
  • by_ref uses [&], so its body works through a reference to the original x.
  • by_val uses [=], so the generated object holds its own copy of the value of x.
  • x is changed to 20 after both lambdas are created.
  • by_ref(15) compares 15 with the current x, which is 20.
  • by_val(15) compares 15 with the copy it holds, which is 10.

What do you think happens?

After x is changed to 20, does by_val(15) return true or false?

  • true

  • false

Reveal answer
Answer:

false

by_val holds its own copy of x, which was 10 when the lambda was written. The change to 20 does not reach that copy, so 15 < 10 is false.

x10x20[&] view of xrefers to x[&] view of xsees 20[=] copy of x10[=] copy of x10referencereference
After the outer x changes from 10 to 20, what does each lambda see?

The syntax of the two lambdas is identical except for the ampersand or equals sign. A reference capture always lets the body see the current state of the original name. A value capture lets the object keep its own copy, separate from the original. Reading the capture list carefully is therefore the habit that tells you which of the two behaviors you will get.

Choosing a Capture Mode

There are four choices to weigh when you write a lambda: capture nothing, capture all used locals by reference, capture all used locals by value, or capture a single named local. Each one changes which outside names the body can reach and how it reaches them. The diagram below lays the four options side by side, grouped by access mode.

[ ]no local names[&]every used local byreference[&x]only x by reference[=]every used local as a copy[=x]only x as a copy
Given the same outer locals, which capture options bring names in, and in which access mode?
Best practice

Use [ ] when the body needs no outside local names. The source identifies this as the form to use in that case, because the lambda then depends on nothing declared around it. Use [&] or [&x] when the body must see the current state of an outer name. Use [=] or [=x] when the object should keep its own copy that does not follow later changes to the original.

Mistakes With Capture Lists

  • Common mistake

    Assuming a value capture shares the original variable

    What it looks like

    auto f = [=](int a){ return a < x; }; followed by a change to x

    Why it fails

    The equals sign gives the generated object a copy of the value, so the object does not follow later changes to x.

    Do this instead

    Use [&] or [&x] when the body must see the current state of the original name.

  • Common mistake

    Leaving the capture list empty while the body uses an outer name

    What it looks like

    auto f = [](int a){ return a < x; }; where x is a local

    Why it fails

    Empty brackets mean the body is given no local names at all, so x is not reachable from the body.

    Do this instead

    Add the name to the capture list, for example [&x] or [=x].

  • Common mistake

    Judging behavior from the body alone

    What it looks like

    Two lambdas with identical bodies, one with [&] and one with [=]

    Why it fails

    The bodies match, but the capture list decides which x each one uses.

    Do this instead

    Always read the capture list first and ask which x the body will use.

Practice Reading Capture Lists

MEDIUM

Given int x = 10; and auto f = [=](int a){ return a < x; };, then x is changed to 20. Does f(15) return true or false, and what does the capture list tell you about the value f uses?

Hints
  • Look at the bare equals sign in the capture list. What does it give the generated object?
  • Compare the copied value of x with the new value of x after the change.
  • The answer should be the same as the by_val case in the earlier example.
MEDIUM

Write a lambda that returns true when its argument is greater than an outer local called limit, and pick the capture list so that the lambda sees later changes to limit. Then explain in one sentence why your choice works.

Hints
  • Pick the capture form that refers to the original rather than a copy.
  • Think about whether [&limit] or [=limit] would follow a later change to limit.

Key Takeaways

  • A lambda expression is a notation that generates a function object, similar in kind to writing Less_than<int>{x}.
  • The capture list sits first in square brackets and decides how the body reaches outside local names.
  • A bare ampersand or equals sign covers every local the body uses; a symbol followed by a name covers only that one name.
  • A reference capture sees the current state of the original, while a value capture keeps the generated object's own copy.
  • Use [ ] when the body needs no outside locals, and choose [&] or [=] (or a single-name form) based on whether the object should follow later changes.

Related concepts

More concepts starting with L