Rationale behind deleting lambda assignment operator?

Viewed 121

I just learned that a (even mutable) lambda in C++ is not assignable unless it has empty captures (c.f. ClosureType::operator=).

Example:

auto x = 0;
auto l0 = [copy = x]() mutable {};
auto l1 = []() mutable {};

static_assert(not std::is_copy_assignable_v<decltype(l0)>);
static_assert(std::is_copy_assignable_v<decltype(std::ref(x))>);
static_assert(std::is_copy_assignable_v<decltype(l1)>);

I'm asking for the rationale behind this choice: Why is the operator= deleted? Particularly in scenarios, where it could be defaulted i.e. the lambda is mutable and all captures are copy-assignable themselves (e.g. l0 in the example above)?

I'm aware of this related question for non-mutable lambdas. But I'd like to understand the decision rather than working around it.

1 Answers

I suspect it was not proposed. Lambdas entered the language much weaker than the function objects they are sugar for and slowly are gaining back functionality. With respect to the special member functions, P0624 proposed adding assignability and default-constructibility for captureless lambdas. Only default-constructibility was proposed in R0, because that is what the author needed and arguably is the most obvious deficiency, but assignability was proposed in R1 based on committee feedback.

Default-constructibility for lambdas with captures certainly is consistent with the language:

auto x1 = [i = 1]() { return i; };
static_assert(not std::is_default_constructible_v<decltype(x1)>); // why??

struct { int i = 1; auto operator()() { return i; } } x2;
static_assert(std::is_default_constructible_v<decltype(x2)>);

Assignability also is consistent and useful. As just one example that comes to mind, there was a proposal at some point to have an analogue of std::default_delete for allocators, i.e. a type that could be used as a template parameter to std::unique_ptr for allocator-allocated pointers. You could imagine using a lambda to capture the allocator and use it for such a purpose:

auto allocator_delete(auto& allocator) {
    using traits = typename std::allocator_traits<std::decay_t<decltype(allocator)>>;
    return [alloc=std::ref(allocator)](typename traits::pointer p) { traits::deallocate(alloc, p, 1); };
}
template<class Alloc> using allocator_deleter_t = decltype(allocator_delete(std::declval<Alloc&>()));
static_assert(not std::is_move_assignable_v<std::unique_ptr<int, allocator_deleter_t<std::allocator<int>>>>);
// why??

But you cannot rebind (move-assign to) this unique_ptr, because the lambda artificially deletes assignment, even though its capture state allows it. Rewrite this as a function object type and the unique_ptr is assignable, with generated assignment operators for the function object type.

That's just one example, but hopefully it clarifies that whether or not you want to assign to the capture state (the std::ref(allocator)) is not the same as what the call operator is allowed to do to the capture state. (The linked question's answer is wrong.)

Related