---
title: "Assignable lambdas with\ncapture"
document: P3963R0
date: 2026-01-15
audience: EWG-I, EWG
reply-to:
  - "Ruslan Arutyunyan (Intel) <ruslan.arutyunyan@intel.com>"
paper-type: proposal
---

# Abstract

This paper proposes making lambdas with captures copy assignable and move assignable when all captured entities are themselves assignable.

# Motivation

When lambda expressions were introduced in C++11, they were not assignable. [[P0624R2]](https://wg21.link/p0624r2) made lambdas without capture assignable. Unfortunately, lambdas with captures have deleted assignment operators, which prevents certain use cases.

One of the examples is interoperability with the C++20 Ranges library that provides powerful abstractions for lazy evaluation pipelines. Range adaptors like `std::views::transform` store callable objects, including lambdas, as part of their state.

Consider the following example:

```cpp
std::vector<int> v = {1, 2, 3, 4, 5};

// This captureless lambda is trivially copyable
auto captureless = [](auto x) { return x + 1; };
static_assert(std::is_trivially_copyable_v<decltype(captureless)>); // ok

// This lambda with capture is trivially copyable
auto with_capture = [y = 1](auto x) { return x + y; };
static_assert(std::is_trivially_copyable_v<decltype(with_capture)>); // ok

// This view is trivially copyable
auto view1 = v | std::views::transform(captureless);
static_assert(std::is_trivially_copyable_v<decltype(view1)>); // ok

// This view is NOT trivially copyable
auto view2 = v | std::views::transform(with_capture);
static_assert(std::is_trivially_copyable_v<decltype(view2)>); // fails
```

In the example above, the lambdas (both with and without capture) are trivially copyable. But while a captureless lambda results in a trivially copyable `transform_view`, a lambda capturing even a simple `int` by value does not. This distinction becomes problematic when targeting parallel range algorithms for heterogeneous execution, as described in the accepted [[P3179R9]](https://wg21.link/p3179r9) proposal.

The thing that makes views not trivially-copyable is *`movable-box`*. It has a non-trivial copy/move assignment operator when the type (in our case, callable) does not model the `copyable` concept. This is exactly what happens with lambdas with capture.

## Workaround is unsatisfactory

Users can work around this limitation by manually writing function objects:

```cpp
// verbose
struct add_y {
    int y;
    auto operator()(auto x) const { return x + y; }
};

auto view = v | std::views::transform(add_y{1});
static_assert(std::is_trivially_copyable_v<decltype(view)>); // OK
```

This defeats the whole purpose of lambda expressions as a concise way to define inline callable objects and makes it verbose again to work with C++ standard algorithms with hand-written callables like it was prior C++11. Users normally don’t favor writing callable objects by themselves until the latter have a significant reusability.

With the [[P3179R9]](https://wg21.link/p3179r9) proposal accepted, users should be able to write simple code like below without hand-written callables:

```cpp
auto result = std::ranges::for_each(
    ext::gpu_policy,
    data | views::transform([val](auto x) { return x + val; }),
    callable);
```

One could say that `ext::gpu_policy` does not belong to the C++ standard; however, the implementations are allowed to have implementation-defined execution policies, so the code above could be standard conformant.

# Proposal

I propose that the assignment operators of a lambda closure type should follow the same rules as the hand-written callable objects: they should behave as if they are not explicitly written (compiler-generated) and then choose the proper behavior based on closure type layout.

In other words:

1. **Copy assignment operator**: If all captures are copy assignable, the closure type’s copy assignment operator should be implicitly defaulted. Otherwise, it should be deleted.
2. **Move assignment operator**: If all captures are move assignable, the closure type’s move assignment operator should be implicitly defaulted. Otherwise, it should be deleted.

Example with capture by value:

```cpp
int y = 1;
auto f = [y](int x) { return x + y; };
auto g = f;  // OK today - copy construction
g = f;       // Currently ill-formed, proposed to be well-formed

static_assert(std::is_copy_assignable_v<decltype(f)>); // Currently fails, proposed to pass
static_assert(std::is_move_assignable_v<decltype(f)>); // Currently fails, proposed to pass
```

Example with capture by reference:

```cpp
int y = 1;
auto f = [&y](int x) { return x + y; };
auto g = f;  // OK today - copy construction
g = f;       // Currently ill-formed, proposed to be ill-formed

static_assert(std::is_copy_assignable_v<decltype(f)>); // Fails, even with this proposal
static_assert(std::is_move_assignable_v<decltype(f)>); // Fails, even with this proposal
```

With this change, the motivating example with `transform_view` (and similar views) would work as expected:

```cpp
auto view = v | std::views::transform([y = 1](auto x) { return x + y; });
static_assert(std::is_trivially_copyable_v<decltype(view)>); // Would pass
```

# Other considerations

## History and semantics

We asked [[P0624R2]](https://wg21.link/p0624r2) authors why they didn’t add assignment operators to lambdas with capture. The answer was that they just wanted to solve their immediate problem without making the proposal broader. So, they don’t see immediate issues with this proposal.

One might argue that assignment to a closure object is semantically questionable; what does it mean to assign one closure to another? The answer is simple. This assignment would do whatever the compiler-generated assignment would normally do for hand-written callables: either be similar to `= default` or similar to be `= delete` if captured objects are references or not assignable. Beyond the main motivation, this change will make lambdas even closer to hand-written callables.

One could ask about a default constructor for lambdas with capture. It is a valid question and we even could potentially say something along those lines: “*if all the captured members are default constructible then the lambda is default constructible or it has a deleted default constructor otherwise*”. However, it would require more exploration and I don’t see the immediate use-case for that.

## Move-only types

It could be that move-only types are captured in a lambda. It is also hard to imagine how it would be possible to create a copy of such an object. So, the example below should be valid C++ but probably unlikely:

```cpp
auto f1 = [p = std::make_unique<int>(1)](int x) { return x + *p; };
auto f2 = std::move(f1);
f1 = std::move(f2);
```

Nevertheless, if one adds `static_assert(std::is_move_assignable_v<decltype(f1)>);` it would pass with this proposal. I do not see any particular problems with that; it’s just worth noting.

## Backward compatibility

This is a pure language extension. Code that previously failed to compile (due to deleted assignment operators) would now compile successfully. No existing valid code would change behavior.

Code that relies on `std::is_copy_assignable_v<T>` trait (where `T` is a lambda with capture) for SFINAE purposes would see changed behavior. This example does not sound realistic because it would work with regular callables with some non-static data members compared to lambdas with captures. Furthermore, such an example did not prevent us from adopting [[P0624R2]](https://wg21.link/p0624r2), thus making captureless lambdas assignable in C++20.

## Other parallel algorithms
vendors

I talked to NVIDIA representatives as a parallel algorithms vendor. They also consider the problem in the Motivation section important and believe that we need to solve it. They don’t have objections to this proposal.

It’s worth mentioning that there is [[P3960R0]](https://isocpp.org/files/papers/P3960R0.html) proposal that is authored by both Intel and NVIDIA. It approaches the similar problem but covers broader scope, thus it is complementary to this proposal. It’s still worth adding lambdas assignability to not pessimize the cases like with `ranges::transform_view` and make them more consistent with other class types.

# Implementation experience

The implementation is not done yet but is not expected to cause problems according to the compiler implementors I talked with.

# Proposed Wording

## Modify [[expr.prim.lambda.closure]](https://wg21.link/expr.prim.lambda.closure)

17 The closure type associated with a *lambda-expression* has no default constructor if the *lambda-expression* has a *lambda-capture* and a defaulted default constructor otherwise. It has a defaulted copy constructor and a defaulted move constructor ([[class.copy.ctor]](https://wg21.link/class.copy.ctor)). It has <del>a deleted copy assignment operator if the *lambda-expression* has a *lambda-capture* and</del> defaulted copy and move assignment operators <del>otherwise</del> ([[class.copy.assign]](https://wg21.link/class.copy.assign)).

[ *Note:* These special member functions are implicitly defined as usual, which can result in them being defined as deleted. — *end note* ]

# References

[P0624R2] Louis Dionne. 2017-11-10. Default constructible and assignable
stateless lambdas.

https://wg21.link/p0624r2

[P3179R9] Ruslan Arutyunyan, Alexey Kukanov, Bryce Adelstein Lelbach.
2025-05-29. C++ parallel range algorithms.

https://wg21.link/p3179r9

[P3960R0] Mark Hoemmen and Ruslan Arutyunyan. Fix or Remove Sender
Algorithm Customization.

https://isocpp.org/files/papers/P3960R0.html
