---
title: "Const/Mutable Extended Lambda Captures"
document: P2034R8
date: 2026-07-01
audience: CWG
reply-to:
  - "Ryan McDougall <mcdougall.ryan@gmail.com>"
  - "Lakshay Garg <lakshayg.xyz@gmail.com>"
---

## Const/Mutable Extended Lambda Captures

| Document | [P2034R8](https://wg21.link/P2034) |
| --- | --- |
| Date | 2026-07-01 |
| Audience | CWG |
| Project | ISO/IEC JTC1/SC22/WG21 14882: Programming Language – C++ |

Ryan McDougall `<mcdougall.ryan@gmail.com>` Authors

Lakshay Garg `<lakshayg.xyz@gmail.com>`

GitHub Issue [https://wg21.link/P2034/github](https://wg21.link/P2034/github)

Source [https://github.com/lakshayg/wg21/tree/main/p2034](https://github.com/lakshayg/wg21/tree/main/p2034)



## Revision History

### Changes from R7: [EWG Discussion](https://wiki.isocpp.org/2026-06_Brno:EvolutionWorkingGroup:P2034R6)

– Changed audience to CWG: EWG forwarded the paper to CWG for inclusion in C++29 (2026-06 Brno).

– Recorded the 2026-06 Brno polls.

– Retained the capture-default syntax `[mutable =]`, `[const&]`, and `[const =]`: EWG reached no consensus either to additionally allow or to substitute the `[=mutable]`, `[&const]`, and `[=const]` spellings.

– Completed the proposed wording for CWG review, drafting the parts previously deferred:

– implicit capture under the qualified capture-defaults `[mutable =]` and `[const =]`, and which explicit captures may accompany a qualified default ([[expr.prim.lambda.capture] paragraph 2](https://eel.is/c++draft/expr.prim.lambda.capture#2));

– the logical-`const` specification of `[const&]`, in [[expr.prim.id.unqual]](https://eel.is/c++draft/expr.prim.id.unqual) and the nested re-capture rule

([[expr.prim.lambda.capture] paragraph 14](https://eel.is/c++draft/expr.prim.lambda.capture#14)); and

– the non-implicit capture of `*this` under a qualified capture-default.

– Unified the qualified by-copy member type with `auto` deduction, stripping both `const` and `volatile`.

– Added a “Wording Design” section explaining the structure of the normative changes.

– Expanded the design discussion: added “Recaptures” and “Redundant Default Captures”, and rewrote

“Const Capture By-reference”.

– Dropped the proposed restriction making a `mutable` capture ill-formed on a `constexpr` or `consteval` lambda: it was unnecessary, since [[expr.const]](https://eel.is/c++draft/expr.const) already governs when such a member may be read.

Broke “Interaction with `consteval` and `constexpr` Lambdas” out into its own section and rewrote it accordingly.

### Changes from R6: [EWG Discussion](https://wiki.isocpp.org/2026-03_Croydon:EvolutionWorkingGroup:P2034R6)

– Restructured the motivation into an initial const-correctness case and a subsequent symmetry-and- simplicity case.

– Added the “Lambdas Are Syntactic Sugar for Function Objects” argument, with standard, compiler, reflection, and implementation evidence.

– Committed const capture to a genuine `const` member (option 3) and unified the mutable and const NSDM type deduction.

– Added the capture-defaults matrix (`[const =]`, `[mutable =]`, `[const&]`).

– Specified const-reference capture as logical const, consistent with the unspecified representation of reference captures.

– Added “Consequences of a `const` Member” and “Teaching `const` Capture”.

– TODO: complete the proposed wording for the capture-defaults and reference cases.

### Changes from R5: [EWG Discussion](https://wiki.isocpp.org/2025-11_Kona:EWGP2034Notes)

– Incorporate extensions into the main proposal.

– Add discussion of capture defaults to the proposal.

– Rearranged some sections and updated links.

– Add wording for:

– mutable captures

– const-ref captures

– const-ref capture-default

– const specifier

### Changes from R4: [EWG Discussion](https://wiki.isocpp.org/2025-06_Sofia:NotesEWGP2034)

– Implementation experience.

### Changes from R3: [EWG-I Discussion](https://wiki.isocpp.org/2024-03_Tokyo:NotesEWGIP2034R2)

– Meta-motivation: safety and security – const should be easier to get right and harder to get wrong.

– Cleaned up some examples.

### Changes from R2

– Update author email addresses.

– Rename `any_invocable` to `move_only_function`.

### Changes from R1

– Add discussion of const captures on move construction and assignment.

– Add vocabulary type `as_mutable`.

– Add alternative implementation strategy for const members.

– Selective move feature in top section.

### Changes from R0: [Concerns from EWG-I](https://wiki.isocpp.org/2020-02_Prague:P2034R0SG17)

– Interactions with `this` pointer.

– Interactions with init-capture packs.

– Clarify const as it applies to pointers.

– Add const-reference use case.

– Expanded prose.

### Polls

#### 2026-06 Brno, R7

D2034R7 should be modified to also allow `[=mutable]`, `[&const]`, and `[=const]` in addition to

`[mutable=]`, `[const&]`, and `[const=]`, with the same semantics respectively: *Not consensus*

SF F N A SA

1 2 6 8 2

D2034R7 should be modified to replace `[mutable=]`, `[const&]`, and `[const=]` with `[=mutable]`,

`[&const]`, and `[=const]` respectively: *Not consensus*

SF F N A SA

2 0 9 7 1

Forward D2034R7 (as on the wiki) to CWG for inclusion in C++29: *Consensus*

SF F N A SA

4 13 2 1 2

#### 2026-03 Croydon, R6

P2034R6 should include default mutable captures: *Strong Consensus in favor*

SF F N A SA

3 28 4 0 0

P2034R6 should explore making const-capture equivalent to a const member: *Strong Consensus in favor*

SF F N A SA

8 25 1 2 0

Encourage more work in the direction of P2034R6: *Strong Consensus in favor*

SF F N A SA

14 26 2 0 0

#### 2025-11 Kona, R5

We [EWG] encourage further work on this paper towards C++29: *Strong Consensus*

SF F N A SA

21 27 5 0 0

#### 2025-06 Sofia, R4

EWG encourages more work in the direction of Partially Mutable Lambda Captures: *Consensus*

SF F N A SA

1 10 4 2 1

EWG encourages more work in the direction of Partially Mutable Lambda Captures, including extensions:

*Stronger consensus*

SF F N A SA

2 15 3 1 0

#### 2024-03 Tokyo, R2

EWGI believes P2034R3 should include a `const` qualifier for lambda captures: *Barely consensus* (Comment:

motivation could be better)

SF F N A SA

2 4 4 1 0

EWGI believes P2034R3 is sufficiently well developed, EWGI forwards it to EWG: *Consensus*

SF F N A SA

3 7 0 0 0

## 1 Background

Lambdas were introduced in N2550, and while previous drafts (N2529) considered mutable capture by value, the original wording left captures entirely const. N2658 salvaged mutable for *all* captures by allowing the `mutable` keyword to modify the call.

`std::move_only_function` (P0288, C++23), and since then `std::copyable_function` (P2548) and `std::function_ref` (P0792) in C++26, improved on `std::function` by respecting the `const` qualifier on their call signature (e.g. `move_only_function<void(int) const>`). A `const`-qualified call type binds only to lambdas that are not marked `mutable` ([[func.wrap.move.ctor]](https://eel.is/c++draft/func.wrap.move.ctor)).

A type that is [“logically const”](https://isocpp.org/wiki/faq/const-correctness#mutable-data-members) is a type that has some members that do not fundamentally change the invariants of the object when mutated, even when it is const.

Taken together, this means the above standard types, and *any* other const-correct callable library, *cannot* work with logically const lambdas in the current form.

## 2 Initial Motivation: Const-correctness

Type erased callables like those above are the backbone of most asynchronous systems. Users of such systems enclose their operations in lambdas and place them in a concurrent queue to be processed elsewhere. Performance is often key in such systems, and such operations may want their own local reusable scratch memory. Or perhaps an accumulator for hysteresis over multiple calls.

```cpp
struct MyRealtimeHandler {
  Callback callback_;
  State state_;
  mutable Buffer accumulator_;
  void operator()(Timestamp t) const {
    callback_(state_, accumulator_, t);
  }
};
concurrent::queue<move_only_function<void(Timestamp) const>> queue;
queue.push(MyRealtimeHandler{f, s});
```

Lambdas in such cases require workarounds, such as abandoning logical const correctness, abandoning ownership, or introducing intermediary {non-}const-propagating types. Strict ownership rules are important due to the asynchronous nature of the handler, and const correctness is important for memory- and thread-safety.

However if we expand lambdas to allow mutable capture, then only the logically mutable non-static data members become mutable, and the rest of the captures can remain const. The idea is illustrated below.

```cpp
Before
                                                  After
struct A {
  State state;
  mutable Buffer buf;
  void operator()() const {
     // ...
  }
};
```

```cpp
// manual bespoke type
move_only_function<void() const> f =
  A{s, b};
                                      move_only_function<void() const> f =
                                      [s, mutable b] { /* ... */ };
template <typename T>
class as_owned_mutable {
  mutable T value;
 public:
  T& ref() const {
   return value;
  }
};
// new vocabulary type
move_only_function<void() const> f =
  [s, b = as_owned_mutable<Buffer>{}]() {
   auto& buffer = b.ref();
   // ...
  };
                                      move_only_function<void() const> f =
                                      [s, mutable b] {
                                         // ...
                                      };
// loss of const correctness
move_only_function<void()> f =
  [s, b]() mutable {
   // ...
  };
                                      move_only_function<void() const> f =
                                      [s, mutable b] {
                                         // ...
                                      };
// loss of ownership
move_only_function<void() const> f =
  [s, buf_ptr = &b]() {
   // ...
  };
                                      move_only_function<void() const> f =
                                      [s, mutable b] {
                                         // ...
                                      };
```

The proposal would allow programmers to apply `const` with simplicity and precision to lambda captures

– improving applicability of const in cases where programmers would otherwise:

1. Declare the lambda blanket mutable.

2. Declare captures by const {non-}propagating wrapper.

Applying `const` with more purpose and simpler syntax would improve the safety and security of such code

– especially for programmers that have learned about the `const` declarations, but are not yet comfortable with `const`-{non-}propagating wrappers. Avoiding use of wrappers also makes lambda captures smaller and thus easier to read and reason about.

Alternatively, if most of the values captured are modifiable, but one should be `const`, then the following would be similarly shorter and more readable. The alternative is to simply leave otherwise const captures modifiable, or to use `std::cref`. The former is less safe, and the latter may be undesirable because the lambda does not own the object referred to, which may create lifetime issues. Moreover it requires a more verbose assignment syntax.

```cpp
Before
                                                      After
template <typename T>
class as_owned_const {
  T value;
 public:
  const T& ref() const {
     return value;
  }
};
```

```cpp
// new vocabulary type
move_only_function<void()> f =
  [s, b = as_owned_const<Buffer>{}] mutable {
   auto& buffer = b.ref();
   // ...
  };
                                         move_only_function<void()> f =
                                         [s, const b] mutable {
                                            // ...
                                         };
// loss of const correctness
move_only_function<void()> f =
  [s, b]() mutable {
   // b can be mutated
  };
                                         move_only_function<void()> f =
                                         [s, const b] mutable {
                                            // ...
                                         };
// loss of ownership
move_only_function<void()> f =
  [s, b = std::cref(buf)]() mutable {
   // ...
  };
                                         move_only_function<void()> f =
                                         [s, const b] mutable {
                                            // ...
                                         };
```

Allowing `const` captures is ergonomic and simple.

## 3 Subsequent Motivation: Symmetry and Simplicity

Our initial motivation only needs a handful of combinations of `const` or `mutable` extensions to lambda syntax in order to meet its use cases – but in subsequent meetings EWG has expressed interest in symmetry and simplicity for its own sake, and asked the authors to investigate the design space.

There seems to be a commonly shared feeling that there is a simpler language hiding in C++, and that the lambda syntax should be orthogonal to all the other ways of declaring callable types, not a microcosm unto itself. The authors agree and have investigated all possible capture possibilities involving `const` or `mutable` captures and operator qualification.

## 4 Design

| Capture Syntax | Description |
| --- | --- |
| `[``mutable` `x]()` `const` `{}` | simple capture of `x` by copy; the NSDM is `mutable` |
| `[``mutable` `x``...``]()` `const` `{}` | simple capture of pack `x`; each NSDM is `mutable` |
| `[``mutable` `x` `=` `init]()` `const` `{}` | init-capture initialized from `init`; the NSDM is `mutable` |
| `[``mutable` `...``xs` `=` `init]()` `const` `{}` | init-capture pack (P0780); each NSDM is `mutable` |

### 4.3.2 Applicability

A mutable capture is permitted on a mutable lambda: it is well-formed, although is redundant in effect.

Note however, a `[x]()` `mutable` `{}` is a different type than a `[``mutable` `x]()` `mutable` `{}`.

```cpp
        () const
                                () mutable
[x]
        struct X {
          int x;
          void operator()() const;
        };
                                struct X {
                                  int x;
                                  void operator()();
                                };
[mutable x]
        struct X {
          mutable int x;
          void operator()() const;
        };
                                struct X {
                                  mutable int x;
                                  void operator()();
                                };
```

### 4.4 Const Capture By-copy

We propose a new form of by-copy capture called “const capture”, which allows lambda captures to be `const` qualified, as shown below. The standard mandates that by-copy captures create a non-static data member (NSDM) in the closure type ([[expr.prim.lambda.capture] paragraph 10](https://eel.is/c++draft/expr.prim.lambda.capture#10)). A const capture, in addition to defining a NSDM, would have the effect of declaring it `const`.

#### 4.4.1 Syntax

| Capture Syntax | Description |
| --- | --- |
| `[``const` `x]()` `mutable` `{}` | simple capture of `x` by copy; the NSDM is `const` |
| `[``const` `x``...``]()` `mutable` `{}` | simple capture of pack `x`; each NSDM is `const` |
| `[``const` `x` `=` `init]()` `mutable` `{}` | init-capture initialized from `init`; the NSDM is `const` |
| `[``const` `...``xs` `=` `init]()` `mutable` `{}` | init-capture pack (P0780); each NSDM is `const` |

#### 4.4.2 Applicability

A const capture is permitted on a const lambda: it is well-formed, although is redundant in effect. Note however, a `[x]()` `const` `{}` is a different type than a `[``const` `x]()` `const` `{}`, and const members create other concerns: see Section 5.1.

```cpp
       () const
                                () mutable
[x]
       struct X {
        int x;
        void operator()() const;
       };
                                struct X {
                                 int x;
                                 void operator()();
                                };
[const x]
       struct X {
        const int x;
        void operator()() const;
       };
                                struct X {
                                 const int x;
                                 void operator()();
                                };
```

### 4.5 Mutable Capture By-reference

We explicitly disallow capture of the form `[``mutable&` `x]`. This is because `mutable` references are not permitted by the language ([[dcl.stc]](https://eel.is/c++draft/dcl.stc)). Note that this is not the same as mutable capture of a reference type:

```cpp
T& x = ...;
auto f = [mutable x]() { }; // closure type gets a `mutable T x;` member
```

### 4.6 Const Capture By-reference

Capture by copy is made `const` by the call operator; capture by reference is not. Two things stand in the way:

First, the `const` on the call operator is *shallow:* it stops you from reassigning a captured pointer or reference, but says nothing about what that pointer or reference binds to. A captured pointer is a member of the entity’s (pointer) type ([[expr.prim.lambda.capture] paragraph 10](https://eel.is/c++draft/expr.prim.lambda.capture#10)), so `const` qualifies the pointer, not its pointee.

```cpp
int i = 5;
int* p = &i;
auto l = [p]() const { *p = 0; };      // ok: const does not reach the pointee
auto x = [p]() const { p = nullptr; }; // error: the captured pointer is const
```

Second, a reference capture need not produce a member at all: an odr-use of it names the original entity

([[expr.prim.lambda.capture]](https://eel.is/c++draft/expr.prim.lambda.capture)), and the standard leaves unspecified whether a member is declared for it

([[expr.prim.lambda.closure]](https://eel.is/c++draft/expr.prim.lambda.closure)) – so there is nothing for `const` to attach to.

Capturing by `const` reference is nonetheless useful – read-only access to an object too large to copy – but today it takes `std::cref` or `std::as_const`, neither as concise nor as discoverable as `const&`.

We therefore depart from analogy with struct members briefly, and define the meaning `[const& x]` directly: within the body, `x` is a `const` lvalue reference, and any nested lambda that re-captures it observes that `const` (see Section 4.10). The usual reference-lifetime caveats apply. Unlike the by-copy cases, this reads the same on a `const` or a `mutable` lambda, because the `const` is on the reference itself rather than supplied by the call operator.

#### 4.6.1 Syntax

```cpp
[const x]
           struct X {
             const int x;
             void operator()() const;
           };
                                               struct X {
                                                 const int x;
                                                 void operator()();
                                               };
```

### 4.5 Mutable Capture By-reference

We explicitly disallow capture of the form `[``mutable&` `x]`. This is because `mutable` references are not permitted by the language ([[dcl.stc]](https://eel.is/c++draft/dcl.stc)). Note that this is not the same as mutable capture of a reference type:

```cpp
T& x = ...;
auto f = [mutable x]() { }; // closure type gets a `mutable T x;` member
```

### 4.6 Const Capture By-reference

Capture by copy is made `const` by the call operator; capture by reference is not. Two things stand in the way:

First, the `const` on the call operator is *shallow:* it stops you from reassigning a captured pointer or reference, but says nothing about what that pointer or reference binds to. A captured pointer is a member of the entity’s (pointer) type ([[expr.prim.lambda.capture] paragraph 10](https://eel.is/c++draft/expr.prim.lambda.capture#10)), so `const` qualifies the pointer, not its pointee.

```cpp
int i = 5;
int* p = &i;
auto l = [p]() const { *p = 0; };      // ok: const does not reach the pointee
auto x = [p]() const { p = nullptr; }; // error: the captured pointer is const
```

Second, a reference capture need not produce a member at all: an odr-use of it names the original entity

([[expr.prim.lambda.capture]](https://eel.is/c++draft/expr.prim.lambda.capture)), and the standard leaves unspecified whether a member is declared for it

([[expr.prim.lambda.closure]](https://eel.is/c++draft/expr.prim.lambda.closure)) – so there is nothing for `const` to attach to.

Capturing by `const` reference is nonetheless useful – read-only access to an object too large to copy – but today it takes `std::cref` or `std::as_const`, neither as concise nor as discoverable as `const&`.

We therefore depart from analogy with struct members briefly, and define the meaning `[const& x]` directly: within the body, `x` is a `const` lvalue reference, and any nested lambda that re-captures it observes that `const` (see Section 4.10). The usual reference-lifetime caveats apply. Unlike the by-copy cases, this reads the same on a `const` or a `mutable` lambda, because the `const` is on the reference itself rather than supplied by the call operator.

#### 4.6.1 Syntax

| Capture Syntax | Description |
| --- | --- |
| `[``const&` `x]()` `mutable` `{}` | simple capture of `x` by `const` reference |
| `[``const&` `x``...``]()` `mutable` `{}` | simple capture of pack `x`, each by `const` reference |
| `[``const&` `x` `=` `init]()` `mutable` `{}` | init-capture binding a `const` reference to `init` |
| `[``const&` `...``xs` `=` `init]()` `mutable` `{}` | init-capture pack (P0780), each binding a `const` reference |

### 4.7 Capture Defaults

The capture-defaults `[=]` and `[&]` may be qualified by `const` or `mutable`, applying the qualifier to every implicitly-captured entity. For symmetry with the explicit reference default `[const&]`, the copy defaults are spelled with an explicit `=`:

`=` (by copy) `&` (by reference)

| (unqualified) | `[``=``]` | `[``&``]` |
| --- | --- | --- |
| `const` | `[``const` `=``]` | `[``const&``]` |
| `mutable` | `[``mutable` `=``]` | ill-formed |

`[const =]` captures every implicitly-captured entity by `const` copy, `[mutable =]` by `mutable` copy, and

`[const&]` by `const` reference. `[mutable&]` is ill-formed, since `mutable` references do not exist ([[dcl.stc]](https://eel.is/c++draft/dcl.stc)).

The current grammar admits only `&` and `=` as a *capture-default* ([[expr.prim.lambda.capture]](https://eel.is/c++draft/expr.prim.lambda.capture)). We extend it:

*capture-default:*

*capture-default-qualifier**opt* *=* `const`*opt* *&* *capture-default-qualifier:*

```cpp
const
mutable
```

This is unambiguous: `const` and `mutable` are keywords, and the trailing `=` or `&` fixes the capture kind.

Following the C++20 deprecation of implicitly capturing `*this` under `[=]` ([[depr.capture.this]](https://eel.is/c++draft/depr.capture.this)), a qualified capture-default does not implicitly capture `*this`; it must be captured explicitly:

```cpp
struct X {
  int x;
  void f() {
    auto a = [mutable =] { return x; };        // error: *this is not captured 
implicitly
    auto b = [mutable =, this] { return x; };  // OK: this captured explicitly
  }
};
```

A qualified capture-default applies its qualifier only to the entities it captures implicitly; an explicitly- listed `this` or `*this` is captured by its own rules and is unaffected by the default’s qualifier. Combined with Section 4.8 – where `this` and `*this` may not themselves be qualified – this fixes every combination:

`[mutable =, this]`, `[const =, *this]`, `[const&, *this]`, and so on capture `this`/`*this` normally, while `[mutable =, const this]` and the like are ill-formed.

#### 4.7.1 Redundant Default Captures

Today, `[=, x]` is ill-formed: under the `=` default `x` is already captured by copy, so naming it again by copy is redundant, and the language rejects it ([[expr.prim.lambda.capture] paragraph 2](https://eel.is/c++draft/expr.prim.lambda.capture#2)). We keep that principle and extend it to the qualified defaults.

The default governs everything captured *implicitly;* an explicit capture in the same list overrides it for one named entity and carries its own qualifier; and the single thing you cannot write is an explicit capture that does *exactly* what the default already does. Under each default the one redundant – and therefore ill-formed

– form is:

| Capture-default | Redundant (ill-formed) explicit capture |
| --- | --- |
| `[``=``]` | `[``=``, x]` |
| `[``mutable` `=``]` | `[``mutable` `=``,` `mutable` `x]` |
| `[``const` `=``]` | `[``const` `=``,` `const` `x]` |
| `[``&``]` | `[``&``,` `&``x]` |
| `[``const&``]` | `[``const&``,` `const&` `x]` |

Pairing each of the five capture-defaults with the five ways to write a single named capture (`x`, `mutable x`, `const x`, `&x`, `const& x`) gives twenty-five combinations: the five above are redundant, and the other twenty compose – an explicit capture overrides the default for its own entity, carrying its own qualifier. A few:

| Capture | Effect |
| --- | --- |
| `[``mutable` `=``,` `const` `x]` | implicit captures mutable; `x` a `const` member |
| `[``const` `=``,` `mutable` `x]` | implicit captures `const`; `x` mutable |
| `[``=``,` `&``x]` | captured by copy; `x` by reference |
| `[``&``, x]` | captured by reference; `x` a copy |
| `[``const&``, x]` | `const`-reference views; `x` a copy |
| `...` | … |

So a default carries the common case while one entity is named as the exception – the case raised in Section 2

(most captures modifiable, one `const`; or the reverse) – without spelling out every capture by hand.

### 4.8 Captures of `this`

A capture may name either `this` or `*this`, and the two capture differently. `[this]` captures the enclosing object *by* reference: the closure conceptually holds the pointer, though the standard leaves it unspecified whether a member is actually declared for it ([[expr.prim.lambda.closure]](https://eel.is/c++draft/expr.prim.lambda.closure)). `[*this]` captures the object *by* copy, declaring an unnamed non-static data member of the enclosing class type

([[expr.prim.lambda.capture] paragraph 10](https://eel.is/c++draft/expr.prim.lambda.capture#10)). We recommend disallowing `const` and `mutable` on all four spellings – `[const this]`, `[mutable this]`, `[const *this]`, and `[mutable *this]` – until experience is accrued.

For `[const this]` and `[mutable this]`, recall that capture is bitwise `const`: a qualifier names the captured pointer, not its pointee. But `this` is a prvalue ([[expr.prim.this]](https://eel.is/c++draft/expr.prim.this)) captured by reference, so there is no by-copy member for the qualifier to attach to; and the captured pointer can never be reassigned, so the qualifier is either meaningless (under the bitwise rule) or inconsistent with it (if read as qualifying `*this`).

For `[const *this]` and `[mutable *this]` the obstacle is mechanical rather than semantic. Because

`*this` is captured by copy, the qualifier *would* carry the same meaning it has on any by-copy capture: a `const` or `mutable` copy of the object. But that member is unnamed and reached implicitly through `this`

– each odr-use of `*this` is rewritten to refer to it ([[expr.prim.lambda.capture]](https://eel.is/c++draft/expr.prim.lambda.capture)) – rather than through a named *id-expression,* so the const-propagation wording this paper threads through [[expr.prim.id.unqual]](https://eel.is/c++draft/expr.prim.id.unqual#4) [paragraph 4](https://eel.is/c++draft/expr.prim.id.unqual#4) and the nested re-capture rule ([[expr.prim.lambda.capture] paragraph 14](https://eel.is/c++draft/expr.prim.lambda.capture#14)) does not reach it.

Rather than special-case that machinery for a capture with no demonstrated demand, we defer it.

### 4.9 Deducing the NSDM Type

A by-copy capture requires a non-static data member ([[expr.prim.lambda.capture] paragraph 10](https://eel.is/c++draft/expr.prim.lambda.capture#10)); the question is its type. The two existing capture forms deduce it by different rules.

A *simple-capture* keeps the captured entity’s type, retaining its cv-qualifiers: the member type is the referenced type if the entity is a reference to an object, an lvalue reference to the referenced function type if it is a reference to a function, and the entity’s type otherwise ([[expr.prim.lambda.capture] paragraph 10](https://eel.is/c++draft/expr.prim.lambda.capture#10)).

Capturing a `const T` therefore yields a `const` member – a deliberate choice (CWG756), so that `decltype`, overload resolution, and template argument deduction inside the lambda agree with the enclosing scope.

An *init-capture* instead behaves “as if it declares … a variable of the form `auto` *init-capture*

`;`” ([[expr.prim.lambda.capture] paragraph 6](https://eel.is/c++draft/expr.prim.lambda.capture#6), N3610, N3648), so its type is deduced by `auto`, which strips top-level cv-qualifiers and references ([[dcl.type.auto.deduct]](https://eel.is/c++draft/dcl.type.auto.deduct)).

#### 4.9.1 `mutable const T`

Applying `mutable` to the cv-preserving simple-capture type can instead yield an ill-formed `mutable const T` – consider for example the following (which is easy to reach in generic code or after a refactor):

```cpp
const int x = 5;
auto f = [mutable x]() { x = 0; };  // ??
```

Because an init-capture deduces by `auto`, it is not affected by this problem: `[mutable x = e]` is `mutable auto x = e;`, and `[const x = e]` is `const auto x = e;`. `auto` never produces a top-level `const`, so `mutable` never collides with one.

| Entity type of `x` | Capture | (1) preserve cv-qualifiers | (2) deduce by `auto` |
| --- | --- | --- | --- |
| `T` | `[``mutable` `x]` | `mutable` `T` | `mutable` `T` |
| `T` | `[``const` `x]` | `const` `T` | `const` `T` |
| `const` `T` | `[``const` `x]` | `const` `T` | `const` `T` |
| `const` `T` | `[``mutable` `x]` | `mutable const T` – ill-formed | `mutable` `T` |
| `volatile` `T` | `[``mutable` `x]` | `mutable` `volatile` `T` | `mutable` `T` |

1. Simple-capture: *preserve the entity’s cv-qualifiers, then add the requested* qualifier. Adding `mutable` over
a `const` entity forms `mutable const T`, which is an error which we simply accept. This is faithful to cv-
preservation but surprising and fragile: whether `[mutable x]` compiles depends on a cv-qualifier the
author may not control.

2. Init-capture: *deduce by* `auto` *rules, then apply the requested* qualifier, exactly as an init-capture does.

`auto` deduction drops any top-level cv-qualifiers, so `mutable` never collides with a `const`; `const` then adds one. This is uniform across both qualifiers and both capture forms.

```cpp
Here we adopt (2): A programmer who writes mutable or const on a capture is requesting a customization, 
so faithfully preserving the source cv-qualifiers – the simple-capture default – is neither expected nor useful; 
deducing as an init-capture does makes a qualified simple-capture and the corresponding init-capture 
produce the same member, and removes the mutable const T hazard.
For an entity of type T:
– mutable produces a mutable member of type std::remove_cvref_t<T>, and
– const produces a member of type const std::remove_cvref_t<T>.
Stripping all top-level cv-qualifiers – not just const – is exactly what auto deduction does, so a qualified 
simple-capture and the corresponding init-capture deduce the same member type even for a volatile 
entity.
An unqualified by-copy capture is unchanged. The qualifier is a genuine member qualifier, not an “as-if” 
treatment confined to the call operator: the closure is exactly the struct a programmer would hand-write, 
so decltype, overload resolution, and reflection (P2996) all observe the real member type.
4.10 Recaptures
[const& x] gives the body a const view of the original object. What nesting raises is the question of what 
an inner lambda sees when it re-captures x:
auto outer = [const& x] {          // x is a const view of the original
  auto inner = [x] { /* ... */ };  // inner makes its own copy of x
};
inner copies x, and because it copies from a const view the copy is itself const – the same long-standing 
rule (CWG756) that makes [x] of a const variable produce a const member. The consequence shows when 
the inner lambda is mutable:
auto outer = [const& x] {
  auto inner = [x]() mutable { x = 0; };   // ill-formed: inner's copy is const
};
Letting inner mutate its own copy would quietly contradict the const view [const& x] promised one line 
above. (The original object is untouched either way – but a modifiable copy silently appearing from a const 
capture is the kind of surprise const-correctness exists to prevent.) The const carries through any depth 
of nesting, including through intervening plain-reference captures: a [const& x] several levels out still 
yields a const copy at the bottom.
None of the following are special cases; each follows from two facts – a const-reference view is const, and 
a copy of a const view is const. The first column is the nesting, read outermost-to-innermost; the second 
is what the innermost x is.
```

| Nesting | Innermost `x` | Why |
| --- | --- | --- |
| `[``const&` `x]{ x; }` | `const` lvalue | the `const`-reference view |
| `[``const&` `x]{ [``&``x]{ x; } }` | `const` lvalue | the view stays `const` through nesting |
| `[``const&` `x]{ [x]{ x; } }` | `const` copy | a copy of a `const` view is `const` |
| `[``const&` `x]{ [x]()` `mutable` `{ x` `=` `0``; } }` | ill-formed | the `const` copy cannot be made |
| mutable`[``const&` `x]{ [``&``x]{ [x]()` `mutable` `{} } }` | `const` copy | `const` carries through the plain-`&` |
| middle`[``&``x]{ [x]()` `mutable` `{ x` `=` `0``; } }` | OK | no `const&` anywhere; an ordinary |

modifiable copy

`[const& x]` behaves the same on a `const` or a `mutable` lambda: the `const` belongs to the view of the object, not to a member supplied by the call operator. And because there is no by-copy member, there is nothing to move or copy when the closure is moved – subject to the usual reference-lifetime caveat.

### 4.11 Interaction with `consteval` and `constexpr` Lambdas

A `mutable` capture raises no new question for a `constexpr` or `consteval` lambda. Mutating and reading local state during constant evaluation has been allowed since C++14 (N3652), and a `mutable` data member is part of that – a function object with a `mutable` member can be evaluated at compile time today:

```cpp
struct Counter {
  mutable int n = 0;
  constexpr int operator()() const { return ++n; }
};
constexpr int f() {
  Counter c;
  return c() + c();          // reads and writes the mutable member at compile time
}
static_assert(f() == 3, ""); // OK since C++14
```

The lambda spelling has worked since lambdas became usable in constant expressions in C++17: an ordinary `mutable` lambda already holds state it mutates and reads.

```cpp
constexpr int g() {
  auto counter = [n = 0]() mutable { return ++n; };
  return counter() + counter();
}
static_assert(g() == 3);     // OK since C++17
```

Writing `[mutable n]` declares the same kind of member as the `mutable` lambda above and behaves the same way. The one case that does not work – reading a `mutable` member of an object that already existed before the evaluation began – is not specific to lambdas: it fails for a hand-written `mutable` member in exactly the same way. So a `mutable` capture needs no rule of its own, and this paper adds none.

### 4.12 Implementation Experience

Ville Voutilainen implemented this proposal, including its extensions, in GCC with regression tests, and reported:

In general, the implementation was very straightforward, after discussing the approach with the maintainer, and coming to the conclusion that it’s simply a matter of adjusting the types of the capture members of lambda for const, and the storage-class-specifier for mutable. The implementation effort was a matter of a single afternoon.

That the change reduces to adjusting capture-member types and storage-class-specifiers is itself evidence for Section 6: the closure is already a class, and the proposal only sets qualifiers on its members. The implementation is available on [GitHub](https://github.com/villevoutilainen/gcc/tree/lambda-p2034) and can be tried on [Compiler Explorer](https://godbolt.org/z/9fcoYeMMf).

## 5 Concerns

### 5.1 Consequences of Const Members

Because the member is genuinely `const`, it carries the ordinary consequences of a `const` data member

– nothing lambda-specific. A `const` member is copied rather than moved by the defaulted move constructor ([[class.copy.ctor]](https://eel.is/c++draft/class.copy.ctor)) – you cannot move from a `const` object – so the closure’s move constructor is `noexcept` only when the member’s *copy* constructor is. Containers notice – `std::vector` reallocation uses `move_if_noexcept`, so a member with a throwing copy (e.g. `std::string`) is copied on every growth:

```cpp
auto concatWith(const std::string x) {  // note the const
  return [x] (std::string y) {          // deduce NSDM as `const std::string`
    return x + y;
  };
}
int main() {
  using Concat = decltype(concatWith(""));
  std::vector<Concat> concats;
  concats.emplace_back(concatWith("A"));
  concats.emplace_back(concatWith("B")); // vector realloc: all elements are copied.
  concats.emplace_back(concatWith("C")); // if Concat had a nothrow move ctor, this
  concats.emplace_back(concatWith("D")); // would have been a move instead.
}
```

This regression is not introduced by the proposal: `[x]` of a `const std::string` already produces a `const` member with exactly this behavior today (CWG756). A const capture only makes the request explicit. Two further consequences follow from the same class rule:

– Assignment. A `const` member also deletes copy and move assignment ([[class.copy.assign]](https://eel.is/c++draft/class.copy.assign)). This is inert while lambdas delete assignment regardless ([[expr.prim.lambda.closure]](https://eel.is/c++draft/expr.prim.lambda.closure)), but P3963 (approved by EWG) restores it for ordinary captures; a const capture then correctly opts back out – exactly as a `const` member of a hand-written callable would.

– Move-only captures. For a move-only captured type the `const` member cannot be copied (the type is move-only) and cannot be moved (a `const` object can only be copied from, never moved) – so the closure is non-movable: a diagnosed error at the use site, not a silent pessimization.

For instance, const-capturing a `unique_ptr` yields a closure that cannot be stored in a `move_only_function`:

```cpp
move_only_function<int()> f =
  [const p = std::make_unique<int>(42)] { return *p; };
  // error: the closure has a const std::unique_ptr<int> member, which can be
  //        neither moved (it is const) nor copied (unique_ptr is move-only),
  //        so the closure is non-movable and move_only_function cannot store it.
```

These are properties of a faithful model (Section 6), not defects to engineer around; the alternative – a non- `const` member behind a `const` spelling – would trade a teachable rule for a hidden one.

### 5.2 Teaching Const Capture

`const` capture behaves as a `const` member because it is one; the rules for using it well are the rules for any `const` member.

– Reach for `const` capture to make genuinely-immutable owned state immutable, not as a reflexive annotation. Its cost is the cost of a `const` member, no more and no less.

– A closure headed for a reallocating container, or one that must be assignable, pays for a `const` capture of an expensive-to-copy type on every move; if that cost matters, do not `const`-capture that member.

– Never `const`-capture a move-only type you must move out of – the closure becomes non-movable.

– To read an object without owning a copy, capture by `const` reference rather than by `const` value; there is then no member to move, subject to the usual reference-lifetime caveat.

– “`const` within the body but a movable member” is a *different* feature – logical versus physical `const` – and is not what `[const x]` means; spelling it `const` would give the keyword two meanings.

### 5.3 East v. West Const

In both East- and West-`const` styles the `const` appears before the identifier; this proposal does not change that.

### 5.4 Pointer to Const v. Const Pointer

Current lambda behavior mandates bitwise `const` – a `const` pointer, not a pointer to `const`. The captured member has the captured entity’s type ([[expr.prim.lambda.capture] paragraph 10](https://eel.is/c++draft/expr.prim.lambda.capture#10)), so qualifying a captured pointer applies `const` to the pointer, not its pointee. This proposal continues that rule and does not modify it.

```cpp
auto c = [const x = ptr]() {
  *x = {};      // ok
  x = nullptr;  // error
};
```

### 5.5 Static Call Operator

A lambda whose call operator is `static` ([P1169](https://wg21.link/p1169)) has no object parameter and may not have a *lambda-capture* ([[expr.prim.lambda.general] paragraph 4](https://eel.is/c++draft/expr.prim.lambda.general#4)), so a `const` or `mutable` capture cannot co-occur with a `static` call operator; the combination is ill-formed.

## 6 Lambdas Are Syntactic Sugar for Function Objects

C++ has converged towards the reality that lambdas are just sugar for a hand-written function object; this proposal only lets the sugar express qualifications the desugared class already supports.

1. The standard specifies the closure as a class.

– [[expr.prim.lambda.closure] paragraph 1](https://eel.is/c++draft/expr.prim.lambda.closure#1) – “a unique, unnamed non-union class type”

– [[expr.prim.lambda.capture] paragraph 10](https://eel.is/c++draft/expr.prim.lambda.capture#10), CWG756 – by-copy captures are non-static data members that retain the entity’s cv-qualifiers

– [[expr.prim.lambda.closure] paragraph 7](https://eel.is/c++draft/expr.prim.lambda.closure#7) – the call operator is a member; special members are

“implicitly defined as usual”

– [[expr.prim.lambda.capture] paragraph 6](https://eel.is/c++draft/expr.prim.lambda.capture#6), N3610, N3648 – an init-capture is defined as an `auto` variable declaration

– N3649 – a generic lambda’s call operator is a member template

2. Compilers represent it as a class.

– Clang: the closure is a `CXXRecordDecl`, each capture a `FieldDecl`, the call operator a `CXXMethodDecl`

([CXXRecordDecl](https://clang.llvm.org/doxygen/classclang_1_1CXXRecordDecl.html), [ASTLambda.h](https://clang.llvm.org/doxygen/ASTLambda_8h_source.html))

– GCC: Ville Voutilainen’s proof-of-concept for this proposal was “adjusting the types of the capture members … and the storage-class-specifier for mutable” – “a single afternoon” ([branch](https://github.com/villevoutilainen/gcc/tree/lambda-p2034))

3. Reflection exposes the captures as ordinary members.

– P2996 – `nonstatic_data_members_of` enumerates a closure’s captures; `type_of` and `is_mutable_member` report each member’s type and `mutable`-ness

– a capture spelled `const` whose member were not `const` would make reflection report a falsehood, so the member must be real

4. Each revision has closed a gap with ordinary classes, never opened one.

– CWG756 – cv-faithful capture members (C++11)

– N3649, N3610, N3648 – generic lambdas and init-captures (C++14)

– P0428, P0780 – explicit template parameters for generic lambdas, and pack-expansion init-captures

(C++20)

– P0624 – captureless lambdas default-constructible and assignable (C++20)

– P2996 – reflection over closure members (C++26)

– P3963 – copy and move assignment for captured lambdas (EWG-approved, pending CWG)

5. It is the orthogonal design.

– `const`, `mutable`, and reference qualifiers mean on a capture exactly what they mean on a member – not a microcosm with bespoke rules; the “simpler language hiding in C++” is the function object the lambda already lowers to

You can run this code today:

```cpp
#include <experimental/meta>
#include <iostream>
template <typename L, std::size_t I>
void print_capture() {
    constexpr auto ctx = std::meta::access_context::unchecked();
    constexpr auto m   = std::meta::nonstatic_data_members_of(^^L, ctx)[I];
    constexpr auto t   = std::meta::type_of(m);
    if constexpr (std::meta::is_mutable_member(m))
        std::cout << "mutable ";
    std::cout << std::meta::display_string_of(t) << '\n';
}
template <typename L>
void print_captures() {
    constexpr auto ctx  = std::meta::access_context::unchecked();
    constexpr auto size = std::meta::nonstatic_data_members_of(^^L, ctx).size();
    std::cout << "capture count: " << size << '\n';
    [&]<std::size_t... I>(std::index_sequence<I...>) {
        (print_capture<L, I>(), ...);
    }(std::make_index_sequence<size>{});
}
int main() {
    auto lam = [x = 42, y = 3.14, z = true]() { return x; };
    print_captures<decltype(lam)>();
    return 0;
}
```

([Compiler Explorer](https://godbolt.org/z/K8xYP47sP): x86-64 clang, `-freflection-latest -std=c++26`)

We are not proposing a new model of lambdas; we are completing one the language has converged on since

C++11 – letting the programmer spell the `const` and `mutable` members the desugared function object could always have held.

### 6.1 Remaining Gaps

The standard deliberately withholds three structural guarantees an ordinary class would give

([[expr.prim.lambda.closure]](https://eel.is/c++draft/expr.prim.lambda.closure)):

1. the declaration order of capture members is unspecified,

2. the implementation may vary their size, alignment, trivial-copyability, and standard-layout-ness, and

3. the closure type is not an aggregate.

None of this is in tension with `const` capture meaning a `const` member: the cv-qualification of a member is a semantic property, independent of where the member sits or whether the type is an aggregate.

Beyond those structural freedoms, a closure is not interchangeable with a hand-written function object in three further ways.

1. Special members, with captures. A closure with captures has no default constructor and a deleted copy
assignment operator ([[expr.prim.lambda.closure]](https://eel.is/c++draft/expr.prim.lambda.closure)); a hand-written struct would have both defaulted.

These are gaps the language is closing on the same trajectory as everything else – captureless lambdas gained them in C++20 (P0624), and P3963 would restore assignment for captured lambdas – not properties this paper changes.

2. Anonymity. A closure type is unique and unnamable: it cannot be forward-declared, and a programmer
cannot add data members, member functions, base classes, or constructors to it. This is inherent to a
lambda being an *expression* rather than a class definition; the sugar generates a fixed shape.

3. The conversion a struct lacks. A captureless closure converts to a function pointer

([[expr.prim.lambda.closure]](https://eel.is/c++draft/expr.prim.lambda.closure)) – a divergence in the opposite direction, an affordance no plain struct has.

The thesis is that the closure *is* a class with a function object’s member semantics, and that `const` and `mutable` on a capture should mean what they mean on a member – not that a lambda is a way to write an arbitrary class. These residual differences are exactly what make a lambda worth having.

## 7 Wording Design

The feature is small, and the wording is mostly small with it: in the common case it sets a cv-qualification and a storage-class-specifier on members the closure already declares. This section is a guide to how the normative changes are organized and why they take the shape they do – the design of the *wording,* as distinct from the design of the feature above. It is written for readers following the proposed wording closely.

The changes touch five subclauses:

– [[expr.prim.id.unqual]](https://eel.is/c++draft/expr.prim.id.unqual) – the type of a name that resolves to a `const`-reference capture;

– [[expr.prim.lambda.general]](https://eel.is/c++draft/expr.prim.lambda.general) – the `const` *lambda-specifier*;

– [[expr.prim.lambda.closure]](https://eel.is/c++draft/expr.prim.lambda.closure) – a note that the `const` *lambda-specifier* has no effect;

– [[expr.prim.lambda.capture]](https://eel.is/c++draft/expr.prim.lambda.capture) – the bulk: grammar, the qualified members, the capture-default rules, and nested re-capture; and

– [[cpp.predefined]](https://eel.is/c++draft/cpp.predefined) – the feature-test macro.

### 7.1 The by-copy member carries most of the feature

By-copy capture already declares a non-static data member for each capture ([[expr.prim.lambda.capture]](https://eel.is/c++draft/expr.prim.lambda.capture#10) [paragraph 10](https://eel.is/c++draft/expr.prim.lambda.capture#10)); all this proposal adds there is that member’s cv-qualification and whether it is `mutable`.

That one paragraph does most of the work.

The member type is stated as prose rather than as `std::remove_cvref_t<T>`, because [[expr]](https://eel.is/c++draft/expr) cannot depend on the library. The two qualified cases reduce to a single helper: from the existing cv-faithful captured type *U,* form *V* by removing top-level cv-qualifiers; a mutable capture yields *V* (declared `mutable`), a const capture yields `const`-qualified *V.* *V* is exactly what `auto` deduction produces, which is why a qualified simple-capture and the corresponding init-capture agree.

Two terms – *captured mutably* and *captured by const copy* – are defined there rather than inlined, because two later places refer to them: the member’s `mutable` storage class and the nested re-capture rule

([[expr.prim.lambda.capture] paragraph 14](https://eel.is/c++draft/expr.prim.lambda.capture#14)). Each term is defined over both the explicit capture (the *capture* begins with the keyword) and the implicit capture (the qualified *capture-default),* so one term serves both

`[mutable x]` and `[mutable =]`.

Function references are the one by-copy capture whose member is a reference rather than a value. A reference member can be neither `const`-qualified nor `mutable` ([[dcl.ref]](https://eel.is/c++draft/dcl.ref), [[dcl.stc]](https://eel.is/c++draft/dcl.stc)), so the qualifier is simply inert there; a note records this rather than carving an exception into the type rule.

### 7.2 The `const` specifier and the specifier constraints

The `const` *lambda-specifier* introduces no behavior – the call operator is already `const` unless `mutable` or `static` is present – so the closure-type wording gains only a note saying so ([[expr.prim.lambda.closure]](https://eel.is/c++draft/expr.prim.lambda.closure#7) [paragraph 7](https://eel.is/c++draft/expr.prim.lambda.closure#7)), and the specifier constraints in [[expr.prim.lambda.general] paragraph 4](https://eel.is/c++draft/expr.prim.lambda.general#4) gain only the entry forbidding `const` alongside an explicit object parameter and the mutual exclusion of `const`, `mutable`, and `static`.

### 7.3 `const&` has no member to qualify

A reference capture need not create a member at all ([[expr.prim.lambda.closure]](https://eel.is/c++draft/expr.prim.lambda.closure)), so `const` has nothing to attach to. The `const` is therefore a property of the name’s *type,* and the one place that already determines the type of a captured name is [[expr.prim.id.unqual]](https://eel.is/c++draft/expr.prim.id.unqual). We add a paragraph there: when a name resolves to an entity captured by `const` reference anywhere in the enclosing chain of lambdas, its type is `const`

-qualified.

We add a separate paragraph rather than widen the existing by-copy rule. Widening was tried – by changing that rule’s trigger from “captured by copy” to “captured” – and it breaks: the rule names “the member that the capture would create” in the innermost capturing lambda, and a reference capture creates no member, so the rule contradicts itself whenever that innermost lambda captures by reference. Keeping the by-copy rule untouched and adding a parallel rule for the reference case avoids the contradiction.

Re-capture is where this needs care. The behavior – a copy of a `const` view is itself `const`, to any nesting depth – is described in Section 4.10; the wording achieves it in the re-capture rule ([[expr.prim.lambda.capture] paragraph 14](https://eel.is/c++draft/expr.prim.lambda.capture#14)), which propagates the `const` by asking whether the entity would have `const`-qualified type within the enclosing lambda, a question it answers through

[[expr.prim.id.unqual]](https://eel.is/c++draft/expr.prim.id.unqual). That phrasing is what carries the `const` through any number of plain-reference intermediaries, but it also makes the two paragraphs refer to each other. The reference is well-founded:

each step moves one lambda outward and terminates at the outermost. Still, it is the part of the wording most worth a second look, and CWG may prefer to restate it as a single inductive definition.

### 7.4 Capture-defaults reduce to one redundancy rule

The existing [[expr.prim.lambda.capture] paragraph 2](https://eel.is/c++draft/expr.prim.lambda.capture#2) forbids an explicit capture that matches the *capture-default,* phrased two ways: a prohibition for `&`, a whitelist for `=`. With qualifiers, the clean generalization is a single prohibition – an explicit *simple-capture* may not capture an entity in exactly the way the default already would. This reproduces today’s diagnostics, treats all five defaults uniformly, and leaves combinations like `[mutable =, const x]` well-formed – which a whitelist would not.

The qualified defaults also do not implicitly capture `*this` ([[expr.prim.lambda.capture] paragraph 7](https://eel.is/c++draft/expr.prim.lambda.capture#7)), continuing the direction of the C++20 deprecation; the unqualified `=` and `&` are unchanged.

### 7.5 What needed no wording

Some cases are handled by omission. The grammar offers no production for a qualified `this` or `*this`, so `[const this]` and the like are ill-formed with no constraint required. The representation of reference captures remains unspecified, so `[const&]` declares no member and needs no member wording. And the `const` *lambda-specifier,* being inert, changes no rule beyond the note noted above.

A `mutable` capture on a `constexpr` or `consteval` lambda likewise needs no constraint of its own; the existing rules in [[expr.const]](https://eel.is/c++draft/expr.const) already settle it. The lvalue-to-rvalue conversion that reads a member is permitted, among other cases, on “a non-volatile glvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of *E”* – an allowance not conditioned on the `mutable` qualifier

– so a `mutable` member is readable whenever the closure was constructed within the evaluation, the usual case for a closure built and called in one constant expression. A `mutable` subobject is excluded only from the *other* allowance, for a glvalue referring to an object “usable in constant expressions”: the definition of *potentially usable in constant expressions* admits only a “non-mutable subobject”, so a `mutable` member of a closure that already existed before the evaluation cannot be read. Both outcomes match a hand-written `mutable` member, so an unmarked but constexpr-suitable lambda is left to fail naturally at use rather than at declaration.

## Proposed Wording

Changes are relative to N5008, using the insert and strike convention.

### [expr.prim.id.unqual]

#### Change [[expr.prim.id.unqual] paragraph 4](https://eel.is/c++draft/expr.prim.id.unqual#4)

If

– the *unqualified-id* appears in a *lambda-expression* at program point P,

– the entity is a local entity or a variable declared by an *init-capture*,

– naming the entity within the *compound-statement* of the innermost enclosing *lambda-expression* of

P, but not in an unevaluated operand, would refer to an entity captured by copy in some intervening *lambda-expression,* and

– P is in the function parameter scope, but not the *parameter-declaration-clause,* of the innermost such *lambda-expression* *E*, then the type of the expression is the type of a class member access expression naming the non-static data member that would be declared for such a capture in the object parameter of the function call operator of *E*.

[Note *3:* If *E* is not declared `mutable` and the entity is not captured mutably

([[expr.prim.lambda.capture]](https://eel.is/c++draft/expr.prim.lambda.capture)) by *E,* the type of such an identifier will typically be `const` qualified.

— *end* note]

#### Add a paragraph after [[expr.prim.id.unqual] paragraph 4](https://eel.is/c++draft/expr.prim.id.unqual#4)

Otherwise, if

– the *unqualified-id* appears in a *lambda-expression* at program point P,

– the entity is a local entity or a variable declared by an *init-capture*,

– naming the entity within the *compound-statement* of the innermost enclosing *lambda-expression* of P, but not in an unevaluated operand, would refer to an entity captured by const reference

([[expr.prim.lambda.capture]](https://eel.is/c++draft/expr.prim.lambda.capture)) in some intervening *lambda-expression,* and

– P is in the function parameter scope, but not the *parameter-declaration-clause,* of the innermost such *lambda-expression*, then the type of the expression is the `const`-qualified type of the entity.

##### [expr.prim.lambda.general]

#### Change [[expr.prim.lambda.general]](https://eel.is/c++draft/expr.prim.lambda.general)

*lambda-specifier:*

```cpp
consteval
constexpr
const
mutable
static
```

#### Change [[expr.prim.lambda.general] paragraph 4](https://eel.is/c++draft/expr.prim.lambda.general#4)

A *lambda-specifier-seq* shall contain at most one of each *lambda-specifier* and shall not contain both `constexpr` and `consteval`. If the *lambda-declarator* contains an explicit object parameter, then no *lambda-specifier* in the *lambda-specifier-seq* shall be `const`, `mutable`, or `static`. The *lambda-specifier-seq* shall not contain both `mutable` and `static` contain at most one of `const`, `mutable`, or `static`. If the *lambda-specifier-seq* contains `static`, there shall be no *lambda-capture*.

##### [expr.prim.lambda.closure]

#### Add a note to [[expr.prim.lambda.closure] paragraph 7](https://eel.is/c++draft/expr.prim.lambda.closure#7)

… It is a non-static member function or member function template that is declared `const` if and only if the *lambda-expression’s* *parameter-declaration-clause* is not followed by `mutable` and the *lambda-declarator* does not contain an explicit object parameter. …

[*Note:* The `const` *lambda-specifier* has no additional effect; the function call operator is declared `const` if and only if `mutable` and `static` are not specified, regardless of whether `const` is present.

— *end* note]

##### [expr.prim.lambda.capture]

#### Change [[expr.prim.lambda.capture]](https://eel.is/c++draft/expr.prim.lambda.capture)

*capture-default:*

*capture-default-qualifier**opt* *=* `const`*opt* *&* *capture-default-qualifier:*

`const` `mutable` *simple-capture:*

`mutable`*opt* *identifier …**opt* `const` *identifier …**opt* `const`*opt* *& identifier …**opt* *this*

**this* *init-capture:*

`mutable`*opt* *…**opt* *identifier initializer* `const` *…**opt* *identifier initializer* `const`*opt* *& …**opt* *identifier initializer*

#### Change [[expr.prim.lambda.capture] paragraph 2](https://eel.is/c++draft/expr.prim.lambda.capture#2)

If a *lambda-capture* includes a *capture-default* that is `&`, no identifier in a *simple-capture* of that *lambda-capture* shall be preceded by `&`. If a *lambda-capture* includes a *capture-default* that is `=`, each *simple-capture* of that *lambda-capture* shall be of the form “`&` *identifier* *…opt”,* “`this`”, or “`* this`”.

If a *lambda-capture* includes a *capture-default,* no *simple-capture* of that *lambda-capture* shall be of the form

– “*identifier* *…opt”* if the *capture-default* is `=`,

– “`mutable` *identifier* *…opt”* if the *capture-default* is `mutable =`,

– “`const` *identifier* *…opt”* if the *capture-default* is `const =`,

– “`&` *identifier* *…opt”* if the *capture-default* is `&`, or

– “`const &` *identifier* *…opt”* if the *capture-default* is `const &`.

#### Change [[expr.prim.lambda.capture] paragraph 6](https://eel.is/c++draft/expr.prim.lambda.capture#6)

An *init-capture* inhabits the lambda scope of the *lambda-expression.* An *init-capture* without ellipsis behaves as if it declares and explicitly captures a variable of the form “`auto` *init-capture* `;`” ignoring any leading `mutable` keyword, except that:

– if the capture is by copy (see below), the non-static data member declared for the capture and the variable are treated as two different ways of referring to the same object, which has the lifetime of the non-static data member, and no additional copy and destruction is performed, and

– if the capture is by reference, the variable’s lifetime ends when the closure object’s lifetime ends.

#### Change [[expr.prim.lambda.capture] paragraph 7](https://eel.is/c++draft/expr.prim.lambda.capture#7)

… the entity is said to be *implicitly captured* by each intervening *lambda-expression* with an associated *capture-default* that does not explicitly capture it, except that `*this` is not implicitly captured by a *lambda-expression* whose *capture-default* is `const =`, `mutable =`, or `const &`. The implicit capture of `*this` is deprecated when the *capture-default* is `=`; see [[depr.capture.this]](https://eel.is/c++draft/depr.capture.this). …

#### Change [[expr.prim.lambda.capture] paragraph 10](https://eel.is/c++draft/expr.prim.lambda.capture#10)

An entity is *captured by copy* if

– it is implicitly captured, the *capture-default* is `==`, `mutable =`, or `const =`, and the captured entity is not `*this`, or

– it is explicitly captured with a capture that is not of the form `this`, `&` *identifier* *…opt,* `const &` *identifier* *…opt* or, `&` *…opt* *identifier initializer* or `const &` *…opt* *identifier* initializer.

An entity captured by copy is *captured mutably* if it is explicitly captured by a *capture* that begins with `mutable`, or it is implicitly captured and the *capture-default* is `mutable =`.

An entity captured by copy is *captured by const copy* if it is explicitly captured by a *capture* that begins with `const`, or it is implicitly captured and the *capture-default* is `const =`.

For each entity captured by copy, an unnamed non-static data member is declared in the closure type. The declaration order of these members is unspecified. The type of such a data member is the referenced type if the entity is a reference to an object, an lvalue reference to the referenced function type if the entity is a reference to a function, or the type of the corresponding captured entity otherwise. The type of such a data member is an lvalue reference to the referenced function type if the entity is a reference to a function. Otherwise, letting *U* be the referenced type if the entity is a reference to an object and the type of the entity otherwise, and *V* be *U* with any top-level cv-qualifiers removed, it is

– *V,* if the entity is captured mutably,

– `const`-qualified *V,* if the entity is captured by const copy, or

– *U* otherwise.

If the entity is captured mutably and is not a reference to a function, the data member is declared `mutable`. *[Note:* For an entity that is a reference to a function, the data member is a reference, which can be neither `const`-qualified nor `mutable` ([[dcl.ref]](https://eel.is/c++draft/dcl.ref), [[dcl.stc]](https://eel.is/c++draft/dcl.stc)); a `const` or `mutable` capture of such an entity therefore has no effect on the data member. — *end* note] A member of an anonymous union shall not be captured by copy.

#### Change [[expr.prim.lambda.capture] paragraph 12](https://eel.is/c++draft/expr.prim.lambda.capture#12)

An entity is *captured by reference* if it is implicitly or explicitly captured but not captured by copy.

An entity captured by reference is *captured by const reference* if it is either explicitly captured with a `const &` capture, or it is implicitly captured and the *capture-default* is `const &`. It is unspecified whether additional unnamed non-static data members are declared in the closure type for entities captured by reference. If declared, such non-static data members shall be of literal type.

#### Change [[expr.prim.lambda.capture] paragraph 14](https://eel.is/c++draft/expr.prim.lambda.capture#14)

If a *lambda-expression* `m2` captures an entity and that entity is captured by an immediately enclosing *lambda-expression* `m1`, then `m2`’s capture is transformed as follows:

– If `m1` captures the entity by copy, `m2` captures the corresponding non-static data member of `m1`’s closure type; if `m1` is not `mutable` and the entity is not captured mutably, the non-static data member is considered to be const-qualified.

– If `m1` captures the entity by reference, `m2` captures the same entity captured by `m1`. If an *id-expression* naming the entity within the *compound-statement* of `m1` would have const-qualified type

([[expr.prim.id.unqual]](https://eel.is/c++draft/expr.prim.id.unqual)), then the entity is considered to be const-qualified for the determination of the type of any non-static data member declared for `m2`’s capture ([[expr.prim.lambda.capture]](https://eel.is/c++draft/expr.prim.lambda.capture#10) [paragraph 10](https://eel.is/c++draft/expr.prim.lambda.capture#10)).

##### Feature-Test Macro

On adoption, bump `__cpp_lambdas` in [[cpp.predefined]](https://eel.is/c++draft/cpp.predefined) to the value corresponding to this paper.

### Thanks

Thanks to Patrick McMichael for suggesting the idea; to Nevin Liber and Matt Calabrese for important corrections; to Nevin Liber, Davis Herring, Barry Revzin, and Victoria Tsai for examples and suggestions; to Hana Dušíková and Ville Voutilainen for observing that the `constexpr`/`consteval` restriction was unnecessary; to Ville Voutilainen for the exploratory implementation; and to Daveed Vandevoorde for feedback on the wording.

<!-- tomd:uncertain:L1252-L1264 -->

Bibliography

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2658.pdf

[CWG756] Dropping cv-qualification on members of closure objects

https://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#756

3)

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3649.html

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2996r12.html

| [N2529] | *Lambda Expressions and Closures: Wording for Monomorphic Lambdas (Revision 3)* |
| --- | --- |
| [http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2529.pdf](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2529.pdf)[N2550] | *Lambda Expressions and Closures: Wording for Monomorphic Lambdas (Revision 4)* |
| [https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2550.pdf](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2550.pdf)[N2658] | *Constness of Lambda Functions (Revision 1)* |

| [N3610] | *Generic lambda-capture initializers, supporting capture-by-move* |
| --- | --- |
| [https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3610.html](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3610.html)[N3648] | *Wording Changes for Generalized Lambda-capture* |
| [https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3648.html](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3648.html)[N3649] | *Wording for Auto Generic Lambda Proposal (Generic (Polymorphic) Lambda Expressions, Revision* |

| [N3652] | *Relaxing constraints on constexpr functions / constexpr member functions and implicit const* |
| --- | --- |
| [https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3652.html](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3652.html)[P0624] | *Default constructible and assignable stateless lambdas* |
| [https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0624r2.pdf](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0624r2.pdf)[P0428] | *Familiar template syntax for generic lambdas* |
| [https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0428r2.pdf](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0428r2.pdf)[P0780] | *Allow pack expansion in lambda init-capture* |
| [https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0780r2.html](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0780r2.html)[P0288] | *move_only_function* |
| [https://wg21.link/p0288](https://wg21.link/p0288)[P0792] | *function_ref: a type-erased callable reference* |
| [https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p0792r14.html](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p0792r14.html)[P2548] | *copyable_function* |
| [https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2548r6.pdf](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2548r6.pdf)[N5008] | *Programming Languages — C++, Working Draft* |
| [https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/n5008.pdf](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/n5008.pdf)[P2996] | *Reflection for C++26* |

[P3963] *Assignable lambdas with capture* [https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3963r0.html](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3963r0.html)
