Capture clauses and capture by value
In the previous lesson (20.6 -- Introduction to lambdas (anonymous functions)), we introduced this example:
Now, let’s modify the nut example and let the user pick a substring to search for. This isn’t as intuitive as you might expect.
This code won’t compile. Unlike nested blocks, where any identifier accessible in the outer block is accessible in the nested block, lambdas can only access certain kinds of objects that have been defined outside the lambda. This includes:
- Objects with static (or thread local) storage duration (this includes global variables and static locals)
- Objects that are constexpr (explicitly or implicitly)
Since search fulfills none of these requirements, the lambda can’t see it.
Tip
Lambdas can only access certain kinds of objects that have been defined outside the lambda, including those with static storage duration (e.g. global variables and static locals) and constexpr objects.
To access search from within the lambda, we’ll need to use a capture clause.
The capture clause
The capture clause is used to (indirectly) give a lambda access to variables available in the
surrounding scope that it normally would not have access to. All we need to do is list the entities we want to
access from within the lambda as part of the capture clause. In this case, we want to give our lambda access to
the value of variable search, so we add it to the capture clause:
The user can now search for an element of our array.
Output
search for: nana Found banana
So how do captures actually work?
While it might look like our lambda in the example above is directly accessing the value of main‘s
search variable, this is not the case. Lambdas might look like nested blocks, but they work
slightly differently (and the distinction is important).
When a lambda definition is executed, for each variable that the lambda captures, a clone of that variable is made (with an identical name) inside the lambda. These cloned variables are initialized from the outer scope variables of the same name at this point.
Thus, in the above example, when the lambda object is created, the lambda gets its own cloned variable named
search. This cloned search has the same value as main‘s
search, so it behaves like we’re accessing main‘s search, but we’re not.
While these cloned variables have the same name, they don’t necessarily have the same type as the original variable. We’ll explore this in the upcoming sections of this lesson.
Key insight
The captured variables of a lambda are copies of the outer scope variables, not the actual variables.
For advanced readers
Although lambdas look like functions, they’re actually objects that can be called like functions (these are called functors -- we’ll discuss how to create your own functors from scratch in a future lesson).
When the compiler encounters a lambda definition, it creates a custom object definition for the lambda. Each captured variable becomes a data member of the object.
At runtime, when the lambda definition is encountered, the lambda object is instantiated, and the members of the lambda are initialized at that point.
Captures are treated as const by default
When a lambda is called, operator() is invoked. By default, this operator() treats
captures as const, meaning the lambda is not allowed to modify those captures.
In the following example, we capture the variable ammo and try to decrement it.
The above won’t compile, because ammo is treated as const within the lambda.
Mutable captures
To allow modifications of variables that were captured, we can mark the lambda as mutable:
Output:
Pew! 9 shot(s) left. Pew! 8 shot(s) left. 10 shot(s) left
While this now compiles, there’s still a logic error. What happened? When the lambda was called, the lambda
captured a copy of ammo. When the lambda decremented ammo from
10 to 9 to 8, it decremented its own copy, not the original
ammo value in main().
Note that the value of ammo is preserved across calls to the lambda!
Warning
Because captured variables are members of the lambda object, their values are persisted across multiple calls to the lambda!
Capture by reference
Much like functions can change the value of arguments passed by reference, we can also capture variables by reference to allow our lambda to affect the value of the argument.
To capture a variable by reference, we prepend an ampersand (&) to the variable name in the
capture. Unlike variables that are captured by value, variables that are captured by reference are non-const,
unless the variable they’re capturing is const. Capture by reference should be preferred over
capture by value whenever you would normally prefer passing an argument to a function by reference (e.g. for
non-fundamental types).
Here’s the above code with ammo captured by reference:
This produces the expected answer:
Pew! 9 shot(s) left. 9 shot(s) left
Now, let’s use a reference capture to count how many comparisons std::sort makes when it sorts an
array.
Possible output
Comparisons: 2 Honda Civic Toyota Corolla Volkswagen Golf
Capturing multiple variables
Multiple variables can be captured by separating them with a comma. This can include a mix of variables captured by value or by reference:
Default captures
Having to explicitly list the variables you want to capture can be burdensome. If you modify your lambda, you may forget to add or remove captured variables. Fortunately, we can enlist the compiler’s help to auto-generate a list of variables we need to capture.
A default capture (also called a capture-default) captures all variables that are mentioned in the lambda. Variables not mentioned in the lambda are not captured if a default capture is used.
To capture all used variables by value, use a capture value of =.
To capture all used variables
by reference, use a capture value of &.
Here’s an example of using a default capture by value:
Default captures can be mixed with normal captures. We can capture some variables by value and others by reference, but each variable can only be captured once.
Defining new variables in the lambda-capture
Sometimes we want to capture a variable with a slight modification or declare a new variable that is only visible in the scope of the lambda. We can do so by defining a variable in the lambda-capture without specifying its type.
userArea will only be calculated once when the lambda is defined. The calculated area is stored in
the lambda object and is the same for every call. If a lambda is mutable and modifies a variable that was
defined in the capture, the original value will be overridden.
Best practice
Only initialize variables in the capture if their value is short and their type is obvious. Otherwise it’s best to define the variable outside of the lambda and capture it.
Dangling captured variables
Variables are captured at the point where the lambda is defined. If a variable captured by reference dies before the lambda, the lambda will be left holding a dangling reference.
For example:
The call to makeWalrus() creates a temporary std::string from the string literal
"Roofus". The lambda in makeWalrus() captures the temporary string by reference. The
temporary string dies at the end of the full expression containing the call to makeWalrus(), but
the lambda sayName still references it past that point. Thus, when we call sayName,
the dangling reference is accessed, causing undefined behavior.
Note that this also happens if "Roofus" is passed to makeWalrus() by value. Parameter
name dies at the end of makeWalrus(), and the lambda is left holding a dangling
reference.
Warning
Be extra careful when you capture variables by reference, especially with a default reference capture. The captured variables must outlive the lambda.
If we want the captured name to be valid when the lambda is used, we need to capture it by value
instead (either explicitly or using a default-capture by value).
Unintended copies of mutable lambdas
Because lambdas are objects, they can be copied. In some cases, this can cause problems. Consider the following code:
Output
1 2 2
Rather than printing 1, 2, 3, the code prints 2 twice. When we created otherCount as a copy of
count, we created a copy of count in its current state. count‘s
i was 1, so otherCount‘s i is 1 as well. Since otherCount is
a copy of count, they each have their own i.
Now let’s take a look at a slightly less obvious example:
Output:
1 1 1
This exhibits the same problem as the prior example in a more obscure form.
When we call myInvoke(count), the compiler will see that count (which has a lambda
type) doesn’t match the type of the reference parameter type (std::function<void()>). It will
convert the lambda into a temporary std::function so that the reference parameter can bind to it,
and this will make a copy of the lambda. Thus, our call to fn() is actually being executed on the
copy of our lambda that exists as part of the temporary std::function, not the actual lambda.
If we need to pass a mutable lambda, and want to avoid the possibility of inadvertent copies being made, there
are two options. One option is to use a non-capturing lambda instead -- in the above case, we could remove the
capture and track our state using a static local variable instead. But static local variables can be difficult
to keep track of and make our code less readable. A better option is to prevent copies of our lambda from being
made in the first place. But since we can’t affect how std::function (or other standard library
functions or objects) are implemented, how can we do this?
One option (h/t to reader Dck) is to put our lambda into a std::function immediately. That way,
when we call myInvoke(), the reference parameter fn can bind to our
std::function, and no temporary copy is made:
Our output is now as expected:
1 2 3
An alternate solution is to use a reference wrapper. C++ provides a convenient type (as part of the
<functional> header) called std::reference_wrapper that allows us to pass a normal type as if
it was a reference. For even more convenience, a std::reference_wrapper can be created by using the
std::ref() function. By wrapping our lambda in a std::reference_wrapper, whenever
anybody tries to make a copy of our lambda, they’ll make a copy of the reference_wrapper instead (avoiding
making a copy of the lambda).
Here’s our updated code using std::ref:
Our output is now as expected:
1 2 3
What’s interesting about this method is that it works even if myInvoke takes fn by
value (instead of by reference)!
Rule
Standard library functions may copy function objects (reminder: lambdas are function objects). If you want to
provide lambdas with mutable captured variables, pass them by reference using std::ref.
Best practice
Try to avoid mutable lambdas. Non-mutable lambdas are easier to understand and don’t suffer from the above issues, as well as more dangerous issues that arise when you add parallel execution.