When we define a lambda, the compiler generates a new (unnamed) class type that corresponds to that lambda. We’ll see how these classes are generated in § 14.8.1 (p. 572). For now, what’s useful to understand is that when we pass a lambda to a function, we are defining both a new type and an object of that type: The argument is an unnamed object of this compiler-generated class type. Similarly, when we use auto
to define a variable initialized by a lambda, we are defining an object of the type generated from that lambda.
By default, the class generated from a lambda contains a data member corresponding to the variables captured by the lambda. Like the data members of any class, the data members of a lambda are initialized when a lambda object is created.
Similar to parameter passing, we can capture variables by value or by reference. Table 10.1 (p. 395) covers the various ways we can form a capture list. So far, our lambdas have captured variables by value. As with a parameter passed by value, it must be possible to copy such variables. Unlike parameters, the value of a captured variable is copied when the lambda is created, not when it is called:
void fcn1()
{
size_t v1 = 42; // local variable
// copies v1 into the callable object named f
auto f = [v1] { return v1; };
v1 = 0;
auto j = f(); // j is 42; f stored a copy of v1 when we created it
}
Because the value is copied when the lambda is created, subsequent changes to a captured variable have no effect on the corresponding value inside the lambda.
We can also define lambdas that capture variables by reference. For example:
void fcn2()
{
size_t v1 = 42; // local variable
// the object f2 contains a reference to v1
auto f2 = [&v1] { return v1; };
v1 = 0;
auto j = f2(); // j is 0; f2 refers to v1; it doesn't store it
}
The &
before v1
indicates that v1
should be captured as a reference. A variable captured by reference acts like any other reference. When we use the variable inside the lambda body, we are using the object to which that reference is bound. In this case, when the lambda returns v1
, it returns the value of the object to which v1
refers.
Reference captures have the same problems and restrictions as reference returns (§ 6.3.2, p. 225). If we capture a variable by reference, we must be certain that the referenced object exists at the time the lambda is executed. The variables captured by a lambda are local variables. These variables cease to exist once the function completes. If it is possible for a lambda to be executed after the function finishes, the local variables to which the capture refers no longer exist.
Reference captures are sometimes necessary. For example, we might want our biggies
function to take a reference to an ostream
on which to write and a character to use as the separator:
void biggies(vector<string> &words,
vector<string>::size_type sz,
ostream &os = cout, char c = ' ')
{
// code to reorder words as before
// statement to print count revised to print to os
for_each(words.begin(), words.end(),
[&os, c](const string &s) { os << s << c; });
}
We cannot copy ostream
objects (§ 8.1.1, p. 311); the only way to capture os
is by reference (or through a pointer to os
).
When we pass a lambda to a function, as in this call to for_each
, the lambda executes immediately. Capturing os
by reference is fine, because the variables in biggies
exist while for_each
is running.
We can also return a lambda from a function. The function might directly return a callable object or the function might return an object of a class that has a callable object as a data member. If the function returns a lambda, then—for the same reasons that a function must not return a reference to a local variable—that lambda must not contain reference captures.
When we capture a variable by reference, we must ensure that the variable exists at the time that the lambda executes.
Rather than explicitly listing the variables we want to use from the enclosing function, we can let the compiler infer which variables we use from the code in the lambda’s body. To direct the compiler to infer the capture list, we use an &
or =
in the capture list. The &
tells the compiler to capture by reference, and the =
says the values are captured by value. For example, we can rewrite the lambda that we passed to find_if
as
// sz implicitly captured by value
wc = find_if(words.begin(), words.end(),
[=](const string &s)
{ return s.size() >= sz; });
If we want to capture some variables by value and others by reference, we can mix implicit and explicit captures:
void biggies(vector<string> &words,
vector<string>::size_type sz,
ostream &os = cout, char c = ' ')
{
// other processing as before
// os implicitly captured by reference; c explicitly captured by value
for_each(words.begin(), words.end(),
[&, c](const string &s) { os << s << c; });
// os explicitly captured by reference; c implicitly captured by value
for_each(words.begin(), words.end(),
[=, &os](const string &s) { os << s << c; });
}
When we mix implicit and explicit captures, the first item in the capture list must be an &
or =
. That symbol sets the default capture mode as by reference or by value, respectively.
When we mix implicit and explicit captures, the explicitly captured variables must use the alternate form. That is, if the implicit capture is by reference (using &
), then the explicitly named variables must be captured by value; hence their names may not be preceded by an &
. Alternatively, if the implicit capture is by value (using =
), then the explicitly named variables must be preceded by an &
to indicate that they are to be captured by reference.
By default, a lambda may not change the value of a variable that it copies by value. If we want to be able to change the value of a captured variable, we must follow the parameter list with the keyword mutable
. Lambdas that are mutable may not omit the parameter list:
void fcn3()
{
size_t v1 = 42; // local variable
// f can change the value of the variables it captures
auto f = [v1] () mutable { return ++v1; };
v1 = 0;
auto j = f(); // j is 43
}
Whether a variable captured by reference can be changed (as usual) depends only on whether that reference refers to a const
or nonconst
type:
void fcn4()
{
size_t v1 = 42; // local variable
// v1 is a reference to a non const variable
// we can change that variable through the reference inside f2
auto f2 = [&v1] { return ++v1; };
v1 = 0;
auto j = f2(); // j is 1
}
The lambdas we’ve written so far contain only a single return
statement. As a result, we haven’t had to specify the return type. By default, if a lambda body contains any statements other than a return
, that lambda is assumed to return void
. Like other functions that return void
, lambdas inferred to return void
may not return a value.
As a simple example, we might use the library transform
algorithm and a lambda to replace each negative value in a sequence with its absolute value:
transform(vi.begin(), vi.end(), vi.begin(),
[](int i) { return i < 0 ? -i : i; });
The transform
function takes three iterators and a callable. The first two iterators denote an input sequence and the third is a destination. The algorithm calls the given callable on each element in the input sequence and writes the result to the destination. As in this call, the destination iterator can be the same as the iterator denoting the start of the input. When the input iterator and the destination iterator are the same, transform
replaces each element in the input range with the result of calling the callable on that element.
In this call, we passed a lambda that returns the absolute value of its parameter. The lambda body is a single return
statement that returns the result of a conditional expression. We need not specify the return type, because that type can be inferred from the type of the conditional operator.
However, if we write the seemingly equivalent program using an if
statement, our code won’t compile:
// error: cannot deduce the return type for the lambda
transform(vi.begin(), vi.end(), vi.begin(),
[](int i) { if (i < 0) return -i; else return i; });
This version of our lambda infers the return type as void
but we returned a value.
When we need to define a return type for a lambda, we must use a trailing return type (§ 6.3.3, p. 229):
transform(vi.begin(), vi.end(), vi.begin(),
[](int i) -> int
{ if (i < 0) return -i; else return i; });
In this case, the fourth argument to transform
is a lambda with an empty capture list, which takes a single parameter of type int
and returns a value of type int
. Its function body is an if
statement that returns the absolute value of its parameter.
Exercise 10.20: The library defines an algorithm named count_if
. Like find_if
, this function takes a pair of iterators denoting an input range and a predicate that it applies to each element in the given range. count_if
returns a count of how often the predicate is true. Use count_if
to rewrite the portion of our program that counted how many words are greater than length 6.
Exercise 10.21: Write a lambda that captures a local int
variable and decrements that variable until it reaches 0. Once the variable is 0 additional calls should no longer decrement the variable. The lambda should return a bool
that indicates whether the captured variable is 0.