So far in this mini-series we’ve looked at capture by reference using [&]
, however you should know that there is another way of accessing variables from the enclosing scope called capture by copy, and this uses the syntax [=]
. Can you guess the output of the program shown below?
#include <iostream>
auto f() {
int a{ 1 };
auto l = [=]{ std::cout << "l(): a = " << a << '\n'; };
a = 2;
l();
a = 3;
l();
a = 4;
return l;
}
int main() {
f()();
}
Perhaps unintuitively, the output is:
l(): a = 1
l(): a = 1
l(): a = 1
As can be deduced from this output, the value of a
is copied into the lambda l()
once, only at the point of declaration, not each time it is invoked. Also, this value is still available within main()
after l()
has gone out of scope; it needs to be since the object returned by f()
is invoked here. The entity of a function object (lambda or functor) returned to an outer scope together with state bound to it, is known as a closure. (Care must be taken to ensure that reference or pointer types used as captured state are still in scope when they are referenced from the outer scope.)
Stateful lambdas that capture by copy are analogous to functors with member variables that hold copies of (rather than references to) state in their enclosing scope. However there is a deliberate twist, as the equivalent functor is not allowed to modify its own state (since its operator()
is declared const
). Here is lambda l()
rewritten as a functor S
, producing the same output:
struct S {
int a;
void operator()() const {
std::cout << "S(): a = " << a << '\n';
}
};
auto f() {
int a{ 1 };
S l{ a };
// ...
So why this restriction? In simple terms, it’s a reminder when writing a lambda that changes to a capture-by-copy variable are lost when the lambda itself goes out of scope since attempting to modify a variable captured in this way results in a compile-time error. There is, however, a way around this restriction, and that is to use a mutable lambda:
auto l = [=]() mutable { std::cout << "l(): a = " << a-- << '\n'; };
(The empty parentheses are necessary in order to allow the keyword mutable
to be placed here.)
The output from running this program is now:
l(): a = 1
l(): a = 0
l(): a = -1
Thus the state of the capture-by-copy variable a
is both modified by, and preserved between, invocations of l()
. Note that there is no way to recover this modified state, unlike when using a functor. (An equivalent functor would omit the const
in the declaration of operator()
.)
To wrap up this mini-series on lambdas, we should mention that more complex capture syntax is available. For example the definition of lambda ll()
below captures a
by copy, b
by reference and “moves” c
(cheaply, without changing its name):
int a;
double b;
std::array<char,16000> c;
auto ll = [a,&b,c=std::move(c)]{ /* lambda body */ };
Before writing overly complex capture clauses, consider whether use of a functor is more appropriate, which would give more fine-grained (and more obvious) control over the state it captures. It is unlikely that lambdas which are passed around as closures will capture purely elementary types, so consideration of their state’s copy/move semantics (and need for correct destruction) is probably advantageous.