Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Variable Capture in Lambdas
Learn lambda captures for writing concise, inline functions.
Lambda Captures
The Problem: Accessing External Variables
In the previous lesson, we introduced this example:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main()
{
constexpr std::array<std::string_view, 5> words{"sky", "blue", "green", "red", "rhythm"};
auto result{std::find_if(words.begin(), words.end(),
[](std::string_view str) {
return (str.find('a') != std::string_view::npos);
})};
if (result == words.end())
std::cout << "Not found\n";
else
std::cout << "Found: " << *result << '\n';
return 0;
}
Now let's modify this to let the user choose which character to search for:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main()
{
constexpr std::array<std::string_view, 5> words{"sky", "blue", "green", "red", "rhythm"};
std::cout << "Search for character: ";
char target{};
std::cin >> target;
auto result{std::find_if(words.begin(), words.end(),
[](std::string_view str) {
return str.find(target) != std::string_view::npos; // ERROR!
})};
if (result == words.end())
std::cout << "Not found\n";
else
std::cout << "Found: " << *result << '\n';
return 0;
}
This won't compile! Unlike nested blocks where inner blocks can access outer variables, lambdas can only access certain kinds of identifiers defined outside the lambda:
- Global variables and static local variables
- Objects with static or thread-local storage duration
- Constexpr objects (explicitly or implicitly)
Since target is a normal local variable, the lambda can't see it.
Lambdas can only access global variables, static locals, and constexpr objects defined outside the lambda.
To give the lambda access to target, we need a capture clause.
The Capture Clause
The capture clause (the [] part of a lambda) allows a lambda to access variables from the surrounding scope. We simply list the entities we want to access:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main()
{
constexpr std::array<std::string_view, 5> words{"sky", "blue", "green", "red", "rhythm"};
std::cout << "Search for character: ";
char target{};
std::cin >> target;
// Capture target by listing it in the capture clause
auto result{std::find_if(words.begin(), words.end(),
[target](std::string_view str) {
return str.find(target) != std::string_view::npos;
})};
if (result == words.end())
std::cout << "Not found\n";
else
std::cout << "Found: " << *result << '\n';
return 0;
}
Sample output:
Search for character: e
Found: blue
How Captures Actually Work
Although it looks like the lambda is directly accessing main's target variable, it's not.
When a lambda is created, for each captured variable, the lambda makes a clone of that variable (with the same name) inside itself. The clone is initialized from the outer scope's variable.
In the example above, when the lambda object is created, it gets its own target member initialized from main's target. The lambda then uses its own copy.
Captured variables are copies of the outer scope variables, not the actual variables themselves. When you write
[target], the lambda creates its own member variable named target that gets initialized from the outer target. The lambda then works with its own copy.
Advanced note: Lambdas are actually objects called functors. When the compiler sees a lambda definition, it creates a custom class. Each captured variable becomes a data member. When the lambda is created at runtime, these members are initialized.
Captures are Const by Default
By default, captured variables are treated as const inside the lambda:
#include <iostream>
int main()
{
int score{100};
auto losePoints{
[score]() {
--score; // ERROR: score is const
std::cout << "Score: " << score << '\n';
}
};
losePoints();
return 0;
}
This won't compile because we're trying to modify a const variable.
Mutable Captures
To modify captured variables, mark the lambda as mutable:
#include <iostream>
int main()
{
int score{100};
auto losePoints{
[score]() mutable {
--score; // OK now
std::cout << "Score: " << score << '\n';
}
};
losePoints();
losePoints();
std::cout << "Actual score: " << score << '\n';
return 0;
}
Output:
Score: 99
Score: 98
Actual score: 100
Wait - the actual score is still 100! The lambda modified its own copy of score, not the original in main. Note that the lambda's copy persists across calls (98 in the second call, not 99).
Because captured variables are members of the lambda object, their values persist across multiple lambda calls.
Capture by Reference
To allow the lambda to modify the original variable, capture by reference using &:
#include <iostream>
int main()
{
int score{100};
auto losePoints{
[&score]() { // & means capture by reference
--score; // modifies main's score
std::cout << "Score: " << score << '\n';
}
};
losePoints();
std::cout << "Actual score: " << score << '\n';
return 0;
}
Output:
Score: 99
Actual score: 99
Now the lambda modifies the original variable. Reference captures are not const (unless the captured variable itself is const).
Prefer reference captures for non-fundamental types (like you would prefer passing by reference to functions).
Example - counting comparisons:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
struct Book
{
std::string_view title{};
std::string_view author{};
};
int main()
{
std::array<Book, 4> books{
{{"1984", "Orwell"},
{"Brave New World", "Huxley"},
{"Fahrenheit 451", "Bradbury"},
{"Animal Farm", "Orwell"}}
};
int comparisons{0};
std::sort(books.begin(), books.end(),
[&comparisons](const auto& a, const auto& b) {
++comparisons; // modifies main's comparisons
return a.author < b.author;
});
std::cout << "Comparisons made: " << comparisons << '\n';
for (const auto& book : books)
std::cout << book.author << ": " << book.title << '\n';
return 0;
}
Possible output:
Comparisons made: 5
Bradbury: Fahrenheit 451
Huxley: Brave New World
Orwell: 1984
Orwell: Animal Farm
Capturing Multiple Variables
Separate multiple captures with commas. You can mix value and reference captures:
int health{100};
int armor{50};
int experience{0};
// Capture health and armor by value, experience by reference
[health, armor, &experience](){};
Default Captures
Explicitly listing every variable can be tedious. A default capture automatically captures all variables mentioned in the lambda.
[=]captures all used variables by value[&]captures all used variables by reference
Example:
#include <algorithm>
#include <array>
#include <iostream>
int main()
{
std::array temperatures{18, 22, 25, 19, 30, 28};
int minTemp{};
int maxTemp{};
std::cout << "Enter min and max temperature: ";
std::cin >> minTemp >> maxTemp;
auto inRange{std::find_if(temperatures.begin(), temperatures.end(),
[=](int temp) { // captures minTemp and maxTemp by value
return temp >= minTemp && temp <= maxTemp;
})};
if (inRange == temperatures.end())
std::cout << "No temperatures in range\n";
else
std::cout << "Found: " << *inRange << '\n';
return 0;
}
You can mix default captures with specific captures:
int health{100};
int armor{50};
int experience{0};
[health, armor, &experience](){}; // explicit
[=, &experience](){}; // capture everything by value except experience (by reference)
[&, armor](){}; // capture everything by reference except armor (by value)
[&, &armor](){}; // ERROR: already capturing by reference
[=, armor](){}; // ERROR: already capturing by value
[armor, &health, &armor](){}; // ERROR: armor listed twice
[armor, &](){}; // ERROR: default capture must be first
Capture Mode Comparison
| Syntax | Meaning | When to Use |
|---|---|---|
[x] |
Capture x by value (const copy) |
Read-only access to small types |
[&x] |
Capture x by reference |
Need to modify original, or large objects |
[=] |
Capture all used variables by value | Multiple read-only captures |
[&] |
Capture all used variables by reference | Multiple variables need modification |
[=, &x] |
Default by value, but x by reference |
Most by value, some by reference |
[&, x] |
Default by reference, but x by value |
Most by reference, some by value |
[x{expr}] |
Init capture: create x from expression |
Computed or transformed captures |
[this] |
Capture this pointer |
Access member variables/functions |
[*this] |
Capture copy of *this (C++17) |
Safe copy when object may be destroyed |
The choice between value and reference capture is similar to choosing between pass-by-value and pass-by-reference for function parameters. Use value captures for small, read-only data. Use reference captures when you need to modify the original or when copying would be expensive.
Defining Variables in the Capture
Sometimes you want to capture a modified version of a variable or define a new variable only visible to the lambda:
#include <algorithm>
#include <array>
#include <iostream>
int main()
{
std::array temperatures{18, 22, 25, 19, 30, 28};
int width{};
int height{};
std::cout << "Enter width and height: ";
std::cin >> width >> height;
auto found{std::find_if(temperatures.begin(), temperatures.end(),
// Define targetTemp visible only to lambda
[targetTemp{width * height}](int temp) {
return temp == targetTemp;
})};
if (found == temperatures.end())
std::cout << "Temperature not found\n";
else
std::cout << "Found: " << *found << '\n';
return 0;
}
targetTemp is calculated once when the lambda is defined and stored in the lambda object. If the lambda is mutable and modifies this variable, the original value is lost.
Only initialize variables in the capture if the value is short and the type is obvious. Otherwise define the variable outside the lambda and capture it.
Dangling Captured References
Variables captured by reference must outlive the lambda. Returning a lambda that captures local variables by reference creates dangling references:
#include <iostream>
#include <string>
auto makeGreeter(const std::string& greeting)
{
// Returns a lambda that captures greeting by reference
return [&]() {
std::cout << greeting << '\n'; // UNDEFINED BEHAVIOR
};
}
int main()
{
auto greet{makeGreeter("Hello")};
greet(); // greeting no longer exists!
return 0;
}
The temporary std::string created from "Hello" is destroyed when makeGreeter returns, but the lambda still references it.
Be careful when capturing by reference, especially with default reference captures. Captured variables must outlive the lambda.
Common Dangling Reference Scenarios
Dangling references occur most often in these situations:
- Returning lambdas from functions (as shown above)
- Storing lambdas in containers or member variables that outlive the captured locals
- Passing lambdas to asynchronous operations (threads, callbacks) where the lambda runs after the original scope ends
- Using
[&]default capture when only some variables need to be references
class Button
{
public:
std::function<void()> onClick{};
void setClickHandler(int localValue)
{
// DANGEROUS: localValue dies when setClickHandler returns
onClick = [&localValue]() {
std::cout << localValue << '\n'; // UNDEFINED BEHAVIOR
};
}
};
To fix this, capture by value instead:
auto makeGreeter(const std::string& greeting)
{
return [greeting]() { // capture by value
std::cout << greeting << '\n';
};
}
Unintended Copies of Mutable Lambdas
Because lambdas are objects, they can be copied. This can cause problems with mutable lambdas:
#include <iostream>
int main()
{
int counter{0};
auto increment{[counter]() mutable {
std::cout << ++counter << '\n';
}};
increment(); // 1
auto otherIncrement{increment}; // copy increment
increment(); // 2
otherIncrement(); // 2 (not 3!)
return 0;
}
Output:
1
2
2
When we copied increment, we copied it in its current state (counter == 1). Now there are two independent lambdas, each with their own counter.
This also happens when passing lambdas to functions. Consider:
#include <iostream>
#include <functional>
void invoke(const std::function<void()>& fn)
{
fn();
}
int main()
{
int counter{0};
auto increment{[counter]() mutable {
std::cout << ++counter << '\n';
}};
invoke(increment);
invoke(increment);
invoke(increment);
return 0;
}
Output:
1
1
1
When we call invoke(increment), the compiler converts increment to a temporary std::function, creating a copy. Each call to invoke makes a new copy, so each sees counter == 0.
Solutions
Option 1: Store the lambda in a std::function from the start:
#include <iostream>
#include <functional>
void invoke(const std::function<void()>& fn)
{
fn();
}
int main()
{
int counter{0};
std::function increment{[counter]() mutable {
std::cout << ++counter << '\n';
}};
invoke(increment); // no copy
invoke(increment); // no copy
invoke(increment); // no copy
return 0;
}
Output:
1
2
3
Option 2: Use std::ref() to pass a reference wrapper:
#include <iostream>
#include <functional>
void invoke(const std::function<void()>& fn)
{
fn();
}
int main()
{
int counter{0};
auto increment{[counter]() mutable {
std::cout << ++counter << '\n';
}};
invoke(std::ref(increment)); // pass reference wrapper
invoke(std::ref(increment));
invoke(std::ref(increment));
return 0;
}
Output:
1
2
3
std::ref() creates a std::reference_wrapper that copies the reference instead of the lambda itself.
Standard library functions may copy lambdas. For mutable lambdas with captures, pass them by reference using `std::ref`.
Avoid mutable lambdas when possible. Non-mutable lambdas are easier to understand, don't have copying issues, and work better with parallel execution.
Quiz
Question 1: Which variables can the lambda use without explicitly capturing them?
int globalVar{};
static int staticGlobal{};
int getValue()
{
return 0;
}
int main()
{
int localVar{};
constexpr int constLocal{};
static int staticLocal{};
static constexpr int staticConstLocal{};
const int constLocalA{};
const int constLocalB{getValue()};
static const int staticConstLocalA{};
static const int staticConstLocalB{getValue()};
[](){
globalVar;
staticGlobal;
localVar;
constLocal;
staticLocal;
staticConstLocal;
constLocalA;
constLocalB;
staticConstLocalA;
staticConstLocalB;
}();
return 0;
}
Show Solution
| Variable | Usable without capture |
|---|---|
globalVar |
Yes (global) |
staticGlobal |
Yes (static storage) |
localVar |
No (automatic storage) |
constLocal |
Yes (usable in constant expression) |
staticLocal |
Yes (static storage) |
staticConstLocal |
Yes |
constLocalA |
Yes (usable in constant expression) |
constLocalB |
No (depends on runtime function call) |
staticConstLocalA |
Yes |
staticConstLocalB |
Yes (static storage) |
Question 2: What does this code print?
#include <iostream>
#include <string>
int main()
{
std::string favoriteFood{"pizza"};
auto printFood{
[=]() {
std::cout << "I like " << favoriteFood << '\n';
}
};
favoriteFood = "sushi";
printFood();
return 0;
}
Show Solution
I like pizza
printFood captured favoriteFood by value. Modifying main's favoriteFood doesn't affect the lambda's copy.
Question 3: (More complex - see original lesson for full quiz)
We're building a number guessing game:
Setup:
- Ask for a starting number
- Ask how many values to generate
- Pick a random multiplier (2-4)
- Generate square numbers starting from the input, each multiplied by the multiplier
Gameplay:
- User guesses values
- If guess matches, remove it and continue
- If guess is wrong, user loses and program shows nearest value
- If user guesses all values, they win
Use std::vector to store numbers, std::find to search, std::vector::erase to remove matches, std::min_element with a lambda to find the closest value.
Show Hint
Use std::abs from <cmath> to calculate distance:
int distance{std::abs(7 - 10)}; // 3
Show Solution
See the original lesson for the complete solution - it's quite long!
Summary
Capture clause: The [] part of a lambda that allows it to access variables from the surrounding scope. Without captures, lambdas can only access global variables, static locals, and constexpr objects.
Capture by value: Listing a variable in the capture clause (e.g., [target]) creates a copy of that variable inside the lambda. The copy is const by default and persists across multiple lambda calls.
Mutable captures: Adding the mutable keyword allows modifying captured variables, but changes only affect the lambda's copy, not the original variable.
Capture by reference: Using & (e.g., [&score]) captures a reference to the original variable, allowing the lambda to read and modify it directly. Reference captures are not const.
Default captures: [=] captures all used variables by value, while [&] captures all used variables by reference. Can be combined with specific captures (e.g., [=, &experience]).
Init captures: Variables can be defined directly in the capture clause (e.g., [targetTemp{width * height}]), creating variables visible only to the lambda.
Dangling references: Variables captured by reference must outlive the lambda. Returning a lambda that captures local variables by reference creates undefined behavior when those locals are destroyed.
Mutable lambda copying problem: When mutable lambdas are copied (explicitly or when passed to standard library functions), each copy gets its own state. Use std::function from the start or pass with std::ref() to avoid unintended copies.
Best practices: avoid mutable lambdas when possible (they're harder to understand and have copying issues), prefer capturing by reference for non-fundamental types, and always ensure reference-captured variables outlive the lambda. For variable-length argument lists or complex state management, consider alternatives to lambdas.
Variable Capture in Lambdas - Quiz
Test your understanding of the lesson.
Practice Exercises
Lambda Captures
Practice using lambda capture clauses to access external variables. Learn capture by value, capture by reference, and default captures.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!