C++ / LAMBDAS AND CALLABLE OBJECTS
Mutable lambdas and stateful callables
Use mutable lambdas to keep and update state inside a closure, and predict how that state behaves when the lambda is copied or passed to algorithms.
What you will learn
- Add mutable to make a by-value capture writable inside the lambda body.
- Predict how state forks when a mutable lambda is copied or passed by value.
- Recover accumulated state from std::for_each's return value or use std::ref.
- Explain why a mutable lambda cannot be called through const F& or const auto.
Understanding Mutable lambdas and stateful callables
A lambda expression creates an object of an unnamed class, and everything captured by value becomes a non-static data member of that class. The generated operator() is const-qualified unless you say otherwise, which is why [n]() { return ++n; } does not compile: inside a const member function those members are const. Writing mutable after the parameter list removes that const qualifier, so the members are writable and the closure becomes a small stateful object that remembers what earlier calls did to it.
The state belongs to the closure object, not to the lambda expression and not to the enclosing variable. The capture copied the outer value once, at the point where the lambda was created, so no amount of mutation in the body ever reaches it. Copying the lambda copies the current member values as well, so each copy continues from where the original stood at the moment of the copy and then drifts away on its own.
This is where mutable lambdas surprise people, because much of the standard library takes callables by value. std::for_each, std::generate, std::thread and constructing a std::function all copy the callable, so the mutation happens inside a copy you do not hold; the fixes are to read back the functor the algorithm returns, to wrap the object in std::ref, or to take it by non-const reference. And because the call operator is no longer const, a mutable closure cannot be invoked at all through a const F& parameter or a const auto variable.
<iostream>
int main() {
int calls = 0;
// auto bad = [calls]() { return ++calls; }; // error: operator() is const here
auto tick = [calls]() mutable { return ++calls; };
std::cout << "tick: " << tick() << '\n';
std::cout << "tick: " << tick() << '\n';
auto snapshot = tick; // copies the closure, whose member currently holds 2
std::cout << "snapshot: " << snapshot() << '\n';
std::cout << "snapshot: " << snapshot() << '\n';
std::cout << "tick: " << tick() << '\n';
std::cout << "outer calls: " << calls << '\n';
}
mutable removes the const from a lambda's call operator, turning captured-by-value data into per-object state that lives in the closure and is duplicated by every copy of it.
Worked examples
Algorithms mutate a copy
Shows that std::for_each accumulates into its own copy of the callable and hands that copy back as its return value.
<algorithm>
<iostream>
<vector>
int main() {
std::vector<int> v{4, 8, 15, 16, 23, 42};
auto adder = [total = 0](int x) mutable { total += x; return total; };
std::for_each(v.begin(), v.end(), adder);
std::cout << "original after for_each: " << adder(0) << '\n';
auto finished = std::for_each(v.begin(), v.end(), adder);
std::cout << "returned functor: " << finished(0) << '\n';
}
Example explained
Line 1[total = 0] is an init-capture: total is a data member of the closure, initialised once when adder is created.
Line 2std::for_each takes its functor by value, so the first call sums into a temporary copy that dies at the end of the statement.
Line 3adder(0) adds nothing, so it just reveals the untouched original state, still 0.
Line 4std::for_each returns that internal copy, so finished holds total == 108, the sum 4+8+15+16+23+42.
A generator that resumes
Passes a two-member mutable closure by non-const reference so the sequence continues, then forks it with a copy.
<iostream>
template <typename F>
void print_n(F& gen, int n) { // F&, not const F&: gen() is a non-const member
for (int i = 0; i < n; ++i) {
if (i) std::cout << ' ';
std::cout << gen();
}
std::cout << '\n';
}
int main() {
auto fib = [a = 0, b = 1]() mutable {
int current = a;
a = b;
b = current + b;
return current;
};
print_n(fib, 5);
print_n(fib, 5);
auto branch = fib; // both now hold the same a and b
print_n(branch, 3);
print_n(fib, 3);
}
Example explained
Line 1The two init-captures a and b are the closure's members; each call rewrites them, which is what makes the sequence advance.
Line 2print_n takes F& deliberately; with const F& the call gen() would be rejected because a mutable lambda's operator() is non-const.
Line 3The second print_n resumes at 5 instead of restarting at 0 because the same closure object was passed by reference.
Line 4branch = fib copies a and b at that instant, so branch and fib then produce identical continuations from separate storage.
Important notes
On a lambda, mutable is not the class-member mutable specifier: it removes the const from operator(), so captures are ordinary non-const members. Before C++23 the empty parameter list is required, so write []() mutable {}, not [] mutable {}.
std::function's operator() is const but still invokes a mutable target, so calling through a const std::function can change hidden state, and every copy of the std::function carries its own copy of that state.
Common mistakes
Capturing by value, forgetting mutable, and writing ++n: the compiler rejects the assignment because operator() is const, and the reflex fix of switching to [&n] quietly changes ownership and dangles if the lambda outlives n.
Expecting [count]() mutable { ++count; } to update the outer count: it never does, because the capture copied count once at creation, so the outer variable stays at its old value and the tally appears to vanish.
Passing a mutable accumulator by value into std::for_each, std::thread or a std::function parameter and then reading the original: the copy did all the work, so the original still reports its starting value.
Try it yourself
Change, predict, then run
Write a mutable lambda next_id that returns 100 on its first call and one more on each call after that. Call it twice, copy it into backup, then call next_id and backup three times each and print both sequences to see exactly where they diverge.
Open the C++ workspaceCheck your understanding
A helper declared as template <class F> void repeat(const F& f, int n) calls f() n times. Passing a mutable lambda to it fails to compile. What is the reason?
- mutable lambdas are not copyable, and binding to const F& requires a copy.
- A mutable lambda's operator() is not const, so it cannot be called on a const object.
- mutable lambdas have no fixed type, so template argument deduction fails for them.
- Captures in a mutable lambda are stored by reference, and references cannot be const-qualified.
Show answer
mutable strips the const qualifier from the closure's call operator, and const F& binds f as a const closure, so invoking a non-const member function on it is ill-formed; changing the parameter to F& fixes it. The copyability answer is tempting because copies of mutable lambdas really do behave surprisingly, but const F& makes no copy at all, and a closure type is copyable whenever its captures are.