A lambda with an empty capture list can only use its parameters and local variables. If the body has to access (read or modify) a variable of the enclosing function, then that variable has to be "captured." This is indicated within the square brackets starting a lambda and can be done by copy or by reference.
Let's imagine a simple counter variable that a lambda wants to access. With no capture at all, the code doesn't compile at all:
#include <iostream>
int main()
{
int count{ 5 };
auto print{ []() { std::cout << count << '\n'; } }; // error
print();
return 0;
}
The name count appears in the lambda body, but the capture list is empty. This doesn't work because the lambda has no access to the enclosing scope's variables. Access is only granted when the variable is either named directly in the brackets or with the default capture mechanisms.
Capture by value results in the lambda getting a private copy of the variable at the time the lambda object itself is constructed. That copy resides inside the function object and is independent of the original object.
#include <iostream>
int main()
{
int count{ 5 };
auto print{ [count]() { std::cout << count << '\n'; } };
count = 10;
print();
return 0;
}
Here [count] is the capture list: this is a request to the compiler to supply a copy of the original count to the lambda's scope. That copy is created at the point in time where count actually does equal 5. Later, when the original count variable becomes 10, it is completely unaffected because the lambda is still working with its own copy of the variable. The captured copy and the original can be modified independently without side effects.
Capture by reference gives the lambda direct access to the original variable. Anything done inside the lambda's body refers to that single original object outside.
#include <iostream>
int main()
{
int count{ 5 };
// Capture this value by reference in the lambda
auto increment{ [&count]() { count = count + 1; } };
increment();
increment();
std::cout << count << '\n';
return 0;
}
The [&count] capture list causes the name count within the lambda's scope to refer directly to the same object existing outside in the parent's scope. Because each call to increment has access to the original, the variable actually increments, resulting in a 7 being output. Changes made by the lambda are visible in the scope of makeLambda, and changes there are visible inside the lambda.
When a number of variables need to be captured, a default mode can be selected for all of them: [=] to capture all seen by reference or [&] to capture all seen by value.
#include <iostream>
int main()
{
int x{ 10 };
int y{ 20 };
// Let the initializer determine this variable type
auto show{ [=]() {
std::cout << x << ' ' << y << '\n';
} };
show();
return 0;
}
[=] captures both seen variables (x and y, in this case) as private copies. The body accesses and prints these copies, giving 10 20 for output. Later changes to the original x and y don't affect the copies captured by the lambda.
The default modes can request reference capture just as well.
#include <iostream>
int main()
{
int x{ 10 };
int y{ 20 };
auto addOne{ [&]() {
x = x + 1;
y = y + 1;
} };
addOne();
std::cout << x << ' ' << y << '\n';
return 0;
}
Here [&] binds the lambda's access to x and y to the original objects, as the bodies of the lambdas both reflect the incremental changes made inside call. The output of 11 21 reflects this shared resource.
Capture by reference offers great power, but the price is the strict lifetime commitment: the lifetime of the lambda should not exceed the lifetime of the referents. If you have code like this:
#include <iostream>
auto makeLambda()
{
int local{ 42 };
return [&local]() { return local; };
}
int main()
{
// Initialize fn with the value returned by makeLambda
auto fn{ makeLambda() };
std::cout << fn() << '\n';
return 0;
}
A local object local is created and then captured by reference by a lambda within its own scope (though not this lambda); this captures it as a reference that lives longer than the created object after the function returns. At that time, the object local no longer exists, but the lambda still refers to that dead memory. Invoking such a lambda yields undefined behavior, it may produce garbage values or cause the program to crash, or it may seemingly work just by chance. Capturing by reference only makes sense while the object referred to is guaranteed to exist.
Value and reference capture therefore make tradeoffs in different directions, with value capture offering an independent object isolated from later modifications and a robust lifetime, at the cost of performance, and reference capture offering tighter access to the actual object at the cost of lifetime obligations. This decision is made at capture site by virtue of the square bracket syntax.
Exercise: Capturing by Value and ReferenceStarter code, hints and solutionOpenClose
Practice: Capturing by Value and Reference
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Call addOne a second time and print labeled x and y values. Explain which original objects the reference captures modify.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
#include <iostream>
int main()
{
int x{ 10 };
int y{ 20 };
auto addOne{ [&]() {
x = x + 1;
y = y + 1;
} };
addOne();
std::cout << x << ' ' << y << '\n';
return 0;
}Hints
The [&] capture lets the lambda refer to the existing x and y. Its assignments therefore change the variables printed after addOne().
Call addOne a second time and update both original variables again. A by-value capture would instead store copies and would not make these same changes to x and y.
Worked solution
The added second call modifies the same x and y captured by reference. Their first printed values are 11 and 21, and after the second call they are 12 and 22; the closure does not store independent integer copies.
#include <iostream>
int main()
{
int x{ 10 };
int y{ 20 };
auto addOne{ [&]() {
x = x + 1;
y = y + 1;
} };
addOne();
std::cout << x << ' ' << y << '\n';
addOne();
std::cout << "After second call: " << x << ' ' << y << '\n';
return 0;
}
Example output
11 21
After second call: 12 22
0 comments
No comments yet. Be the first.