---
title: 'Lambdas and Function Objects'
source: 'https://academia.sh/en/courses/cpp-standard-library/lambdas-and-function-objects'
course: 'Resource Management and the Standard Library'
language: en
updated: '2026-09-26T03:02:47+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that a lambda capturing by value does not change the outer variable itself'
    - 'Shows how the mutable keyword makes the captured copy modifiable'
    - 'Distinguishes capture by reference from capture by value'
    - 'Recognizes that a lambda is an unnamed function object the compiler generates'
---

# Lambdas and Function Objects

The measurement of how a lambda's non-mutable capture-by-value leaves the source unchanged, and how the captured copy stays independent across calls.

The previous lesson searched with `std::remove` by passing it a value; but
some algorithms ask for a **predicate** instead of a value — something
callable that evaluates a condition. This lesson moves to the most common
form of that callable thing, lambda expressions. When a lambda
**captures** an outer variable — by writing `[value]` inside the square
brackets — what happens to that outer variable?

## Capture by Value: Copying, Not Binding

A lambda that writes `[value]` embeds a **copy** of the `value` variable
inside the lambda object. This is not holding a reference to the outer
`value`; a snapshot of the value is taken at the moment the lambda is
created.

```cpp
// capture-by-value.cpp — does the lambda change the outer variable
#include <cstdio>

int main() {
    int value = 10;
    auto show = [value]() { std::printf("captured value %d\n", value); };

    value = 99;
    show();

    std::printf("outer value %d\n", value);
    return 0;
}
```

```text
captured value 10
outer value 99
```

**SL26.** When the lambda was created, `value` was `10`, and the lambda
embedded a copy of that `10`. Before the lambda was called, the outer
`value` was assigned `99`, but when `show()` was called it still printed
`10` — because the copy inside the lambda is completely independent from
the outer variable. The outer `value` itself also stayed **unchanged**:
the act of creating the lambda never modified the variable it captured;
the line `value = 99` was already a separate assignment operation, not
the "capture" operation this lesson measures.

## `mutable`: Making the Captured Copy Modifiable

By default, a copy captured by value is treated as `const` inside the
lambda's call operator — it cannot be modified. The `mutable` keyword
removes this restriction and allows the captured copy to be modified
inside the lambda's body. This change affects only the copy, not the
outer variable.

```cpp
// mutable-capture.cpp — modifying the copy with mutable
#include <cstdio>

int main() {
    int step = 0;
    auto advance = [step]() mutable {
        step += 1;
        std::printf("step inside lambda %d\n", step);
    };

    advance();

    std::printf("outer step %d\n", step);
    return 0;
}
```

```text
step inside lambda 1
outer step 0
```

**SL27.** Thanks to `mutable`, the copy of `step` inside the lambda
changed from `0` to `1` during the `advance()` call — the lambda object
was able to make this change on its own copy. But the outer `step`
remained `0` after the call as well: **unchanged**. This clarifies what
`mutable` changes — not the outer variable, only the copy the lambda
embeds inside itself. This is where this course's central measurement
finds its answer for this lesson: capture by value, no matter what
`mutable` is written, never changes the outer source.

## Capture by Reference: Direct Access to the Outer Variable

Writing `[&value]` captures a reference to the outer variable instead of
taking a copy. In this case, a change inside the lambda's body also
changes the outer variable — because there is no longer a separate copy.

```cpp
// capture-by-reference.cpp — capture by reference changes the outside
#include <cstdio>

int main() {
    int step = 0;
    auto advance = [&step]() { step += 1; };

    advance();

    std::printf("outer step %d\n", step);
    return 0;
}
```

```text
outer step 1
```

**SL28.** This time there was no need to write `mutable`, because what
was being modified was not a copy but the outer `step` itself. After the
`advance()` call, the outer `step` went from `0` to `1`: the kind of this
operation is **changed**. This stands directly opposite the
capture-by-value example at the start of the lesson: `[value]` left the
source unchanged while `[&step]` changes it. The difference is a single
character in the capture list (`&`), but the result is the exact
opposite.

## Bulk Capture: `[=]`, `[&]`, and Mixed Lists

Instead of writing every outer variable one by one, `[=]` captures all
outer variables by value, and `[&]` captures all of them by reference.
The two can also be mixed: `[=, &value]` captures everything else by
value and only `value` by reference; `[&, value]` does the exact
opposite. Bulk capture leaves the choice of which variable to capture to
the compiler — every outer name used in the lambda's body is
automatically added to the list.

```cpp
// bulk-capture.cpp — mixed capture list
#include <cstdio>

int main() {
    int threshold = 10;
    int step = 0;

    auto evaluate = [=, &step](int value) {
        if (value > threshold) {
            step += 1;
        }
    };

    evaluate(15);

    std::printf("outer step %d\n", step);
    std::printf("outer threshold %d\n", threshold);
    return 0;
}
```

```text
outer step 1
outer threshold 10
```

**SL29.** In the capture list `[=, &step]`, `threshold` was captured by
value (as a copy), and `step` was captured by reference. The call
`evaluate(15)` read the copy of `threshold` but did not change it — the
outer `threshold` stayed **unchanged**. The same call **changed**
`step` through the reference: the outer `step` went from `0` to `1`. A
mixed capture list makes it possible for some variables in the same
lambda to stay unchanged while others change at the same time — the kind
decision is made separately for each name in the capture list; there is
no single kind for the list as a whole.

## A Lambda Is Actually a Function Object

When the compiler sees a lambda expression, it generates an unnamed class
behind the scenes: this class has an `operator()` member function, and
the captured variables are that class's data members. A `[value]`
capture adds a member of `value`'s type to the generated class and
copies that member in the constructor. This explains why lambdas can be
used in the same places as hand-written function objects (ordinary
classes that define an `operator()`).

```cpp
// function-object-equivalent.cpp — a lambda and a hand-written class do the same job
#include <cstdio>

struct ValueDisplayer {
    int value;
    void operator()() const { std::printf("value inside class %d\n", value); }
};

int main() {
    int value = 42;

    ValueDisplayer class_displayer{value};
    auto lambda_displayer = [value]() { std::printf("value inside lambda %d\n", value); };

    class_displayer();
    lambda_displayer();
    return 0;
}
```

```text
value inside class 42
value inside lambda 42
```

**SL30.** The `ValueDisplayer` class and the `lambda_displayer` lambda
produced the same result, because both apply the same pattern: carrying
a value as a member and reading it inside `operator()`. Neither of these
calls changed `value`; both leave it **unchanged**, because both took
their own copy in the constructor and only read it. This equivalence
shows that lambdas are not a "special" language feature, but an ordinary
class the compiler writes automatically.

## What the Capture List Leaves in the Source

The "Systems Programming with C" course (`c-systems-programming`) asked
whether a change **reaches** the other side: the writer's own copy,
parties sharing the same object, or no one at all. The capture list
immediately brings that question to mind — a lambda capturing with
`[&value]` "sees" the outer change, one capturing with `[value]` does
not. This lesson does not ask that question. What is measured here is
what is left **in the source itself** after the lambda is called: the
source is **unchanged** with capture by value, **changed** with capture
by reference, and **unspecified** when it was moved through an rvalue
capture. Seeing names a relationship; what is left behind names a state.
This course's measure is the state.

## `std::function`: Keeping Different Callables in the Same Type

Every lambda has its own unique, unnamed type generated by the compiler —
two different lambda expressions are different types even if they carry
the same signature. `std::function<Result(Arguments...)>` is a wrapper
that lets these different types be kept in a single common container.
The act of wrapping does not change the callable being wrapped; it only
carries a copy of it.

```cpp
// std-function-wrapping.cpp — does std::function change the lambda it wraps
#include <cstdio>
#include <functional>

int main() {
    int multiplier = 3;
    auto lambda = [multiplier](int value) { return value * multiplier; };

    std::function<int(int)> wrapped = lambda;
    multiplier = 100;

    std::printf("wrapped(5) result %d\n", wrapped(5));
    std::printf("outer multiplier %d\n", multiplier);
    return 0;
}
```

```text
wrapped(5) result 15
outer multiplier 100
```

**SL31.** The line `std::function<int(int)> wrapped = lambda;` took a
copy of `lambda`'s state at that moment into `wrapped` — this is a
copying operation, just like the `[multiplier]` capture itself.
`multiplier` was later assigned `100`, but `wrapped(5)` still gave `15`
(`5 * 3`), because the copy inside `wrapped` carried `multiplier`'s
value from before the assignment. `lambda` itself came out of this
wrapping operation **unchanged**: assignment to `std::function` never
wrote to the `lambda` object, it only copied it. This is the same
pattern as the capture-by-value example at the start of the lesson — the
difference is that this time what is copied is not an `int`, but a
lambda object itself.

This copying carries a cost: to be able to hold callables of different
types in a single common type, `std::function` applies **type erasure**
— it hides the real type of the callable it wraps behind a base class.
This hiding usually requires a dynamic memory allocation and adds an
indirect jump (a mechanism similar to a virtual call) on every call. A
lambda assigned directly to an `auto` variable keeps its own concrete
type; the compiler can inline the call directly. `std::function` is
therefore not a tool that stands in for `auto` everywhere — it is
preferred when callables of different types need to be carried in the
same container (for example, keeping a list of callbacks in a
container), not just to hold a single lambda.

## Generic Lambdas: `auto` as the Parameter Type

Starting with C++14, a lambda's parameter list can write `auto` instead
of a concrete type. This turns the lambda's call operator into a
template: the same lambda, when called with different types, generates a
separate `operator()` instance for each type. This has nothing to do
with the capture list — the kind (unchanged/changed) of the captured
variables is still determined by the same rules; only the call
parameter is generic.

```cpp
// generic-lambda.cpp — an auto-parameter lambda works with different types
#include <cstdio>

int main() {
    auto show = [](auto value) {
        std::printf("show called with a value\n");
        (void)value;
    };

    show(7);
    show(3.14);

    return 0;
}
```

```text
show called with a value
show called with a value
```

**SL32.** The `show` lambda was called with both an `int` and a
`double`; the compiler generated a separate `operator()` instance for
each call, but there is a single lambda expression in the source code.
This is the same mechanism as the general behavior of template
functions — an `auto` parameter turns the lambda's call operator into a
template member function. This has no direct bearing on this lesson's
central axis (the kind of the captured variable); a generic lambda only
makes the type of the *call parameter* flexible, it does not change the
capture list's behavior.

## If `mutable` Is Not Written

Inside the body of a lambda without `mutable`, a line that tries to
assign directly to a variable captured by value **does not compile** —
because `operator()` is generated as `const` unless `mutable` is
written, and a `const` member function cannot assign to data members.

```cpp
// const-lambda-body.cpp — why assignment fails to compile without mutable
#include <cstdio>

int main() {
    int value = 0;
    auto read = [value]() { std::printf("value %d\n", value); };

    // The line below does not compile, because operator() is const
    // and value cannot be modified:
    // auto broken = [value]() { value += 1; };

    read();
    return 0;
}
```

```text
value 0
```

This restriction is not a shortcoming, it is a guarantee: unless
`mutable` is written, it is enforced at compile time that a lambda will
read the same captured value the same way on every call, without
silently accumulating state across calls. If a lambda is meant to hold
state across calls, that intent must be stated explicitly by writing
`mutable`.

This guarantee also determines which question a reader must ask when
examining a lambda: is there an `&` in the capture list or not, and if
so, is `mutable` written. The answer to these two questions alone
determines, for every variable the lambda captures, whether the source
will remain **unchanged** or become **changed** — no runtime observation
is needed either.

## The Bounding Measurement

All three lambdas carry the same body — increment the captured name once
and print it — but the moment only the capture form changes, the kind of
the outer source splits into three:

```cpp title="three-captures-three-kinds.cpp"
#include <cstdio>
#include <string>
#include <utility>

int main() {
    int step = 0;
    auto by_value = [step]() mutable {
        ++step;
        std::printf("inside body: %d\n", step);
    };
    by_value();

    int position = 0;
    auto by_reference = [&position]() mutable {
        ++position;
        std::printf("inside body: %d\n", position);
    };
    by_reference();

    std::string s = "data";
    auto by_move = [t = std::move(s)]() {
        std::printf("inside body: %s\n", t.c_str());
    };
    by_move();
    s = "new-value";

    std::printf("outer step: %d\n", step);
    std::printf("outer position: %d\n", position);
    std::printf("outer s: %s\n", s.c_str());
    return 0;
}
```

```text title="output"
inside body: 1
inside body: 1
inside body: data
outer step: 0
outer position: 1
outer s: new-value
```

All three captures work as expected inside the body, but the trace they
leave on the outer source falls into three different kinds. `[step]`
captures by value; even though `mutable` allows the copy to be
incremented, this increment affects only the lambda's own copy, and the
outer `step` stays **unchanged** at `0`. `[&position]` captures by
reference; `++position` inside the body directly changes the outer
variable, and the outer `position` **changed** to `1`. `[t =
std::move(s)]` is an rvalue capture: the resource `s` held is moved into
`t`, and after the move, `s` is left in a valid but **unspecified**
state — this is why this lesson never reads or prints `s`'s content or
length after the move. What makes `s` usable again is assigning it a new
value: after the line `s = "new-value"`, `s` has defined content again,
and it is safe to read it — the last line of output shows this. The same
body, with three different capture forms, leaves the source in three
different kinds: unchanged, changed, unspecified.

## Summary

- Capture by value with `[value]` takes a copy of the outer variable; the
  outer variable comes out of the lambda-creation operation
  **unchanged**.
- `mutable` makes the captured copy modifiable inside the lambda's body,
  but this change affects only the copy, not the outer source.
- Capture by reference with `[&value]` accesses the outer variable
  itself; in this case a change inside the lambda's body also
  **changes** the outside.
- A lambda is an instance of an unnamed class the compiler generates; the
  captured variables are that class's data members, and the lambda's
  body is its `operator()`.
- The kind of this lesson's central measurement is **unchanged**: with
  or without `mutable`, capture by value never changes the outer source.
- The bounding measurement: the same body produces three different
  kinds with three different captures — capture by value is
  **unchanged**, capture by reference is **changed**, and rvalue
  (move) capture leaves the source **unspecified**, and this lesson
  never reads that source before reassigning it.

## Next Step

This lesson showed how a lambda carries the value it captures inside
itself, when `mutable` makes that copy modifiable, and why capture by
reference directly affects the outer source. The next lesson takes up a
way to view a container's elements without transforming them and without
building a new container: how does a range view, as a window looking
into the underlying container, leave the source unchanged?
