[2026] C++ Lambda Capture — Value, Reference, and Init Capture
이 글의 핵심
Lambda capture defines how a C++ lambda reaches outer variables. Lambdas can capture names from their enclosing scope by copy or by reference, depending on the capture list.
What is lambda capture?
Lambda capture defines how a lambda accesses variables from its surrounding scope. A lambda can capture names from the scope where it is defined and use them inside the body, either by copy or by reference, depending on the capture list.
Example C++:
int x = 10;
// Capture by value
auto f1 = [x]() { return x; };
// Capture by reference
auto f2 = [&x]() { return x; };
// Capture all locals by value
auto f3 = [=]() { return x; };
// Capture all locals by reference
auto f4 = [&]() { return x; };
Why capture matters:
- Closures: the lambda can remember outer state.
- Flexibility: choose value vs reference per variable.
- Concision: less boilerplate than a hand-written functor.
- Type safety: the compiler checks what you capture.
// Functor: verbose
struct Adder {
int x;
Adder(int x) : x(x) {}
int operator()(int y) const { return x + y; }
};
Adder add10(10);
std::cout << add10(5) << '\n'; // 15
// Lambda capture: short
int x = 10;
auto add10 = [x](int y) { return x + y; };
std::cout << add10(5) << '\n'; // 15
How capture works:
A lambda is lowered to an anonymous function object (functor). Captured variables become data members of that object.
int x = 10;
auto f = [x]() { return x; };
// Conceptually similar to:
struct __lambda {
int x;
__lambda(int x) : x(x) {}
int operator()() const { return x; }
};
__lambda f(x);
Capture by value vs by reference
Example C++:
int x = 10;
// By value: a copy lives in the closure
auto f1 = [x]() mutable {
x++; // modifies the copy
return x;
};
cout << f1() << endl; // 11
cout << x << endl; // 10 (original unchanged)
// By reference: aliases the original
auto f2 = [&x]() {
x++; // modifies the original
return x;
};
cout << f2() << endl; // 11
cout << x << endl; // 11 (original changed)
Mixed capture
Example C++:
int x = 10;
int y = 20;
// x by value, y by reference
auto f = [x, &y]() {
// x++; // error: by-value capture is const unless mutable
y++; // OK: reference capture
return x + y;
};
cout << f() << endl; // 31
cout << x << endl; // 10
cout << y << endl; // 21
Init capture (C++14)
Example C++:
// Introduce a new capture name
auto f1 = [x = 42]() {
return x;
};
// Move capture
auto ptr = make_unique<int>(10);
auto f2 = [p = move(ptr)]() {
return *p;
};
// Expression in the capture
int x = 10;
auto f3 = [y = x * 2]() {
return y;
};
cout << f3() << endl; // 20
Practical examples
Example 1: Counter
Implementation sketch for makeCounter:
auto makeCounter() {
int count = 0;
return [count]() mutable {
return ++count;
};
}
int main() {
auto counter = makeCounter();
cout << counter() << endl; // 1
cout << counter() << endl; // 2
cout << counter() << endl; // 3
}
Example 2: Filter
vector<int> filterGreaterThan(const vector<int>& vec, int threshold) {
vector<int> result;
copy_if(vec.begin(), vec.end(), back_inserter(result),
[threshold](int x) {
return x > threshold;
});
return result;
}
int main() {
vector<int> nums = {1, 5, 3, 8, 2, 9, 4};
auto filtered = filterGreaterThan(nums, 5);
for (int n : filtered) {
cout << n << " ";
}
cout << endl; // 8 9
}
Example 3: Event handler
class Button {
private:
function<void()> onClick;
public:
void setOnClick(function<void()> handler) {
onClick = handler;
}
void click() {
if (onClick) {
onClick();
}
}
};
int main() {
Button button;
int clickCount = 0;
// Capture clickCount by reference
button.setOnClick([&clickCount]() {
clickCount++;
cout << "Clicked " << clickCount << " time(s)" << endl;
});
button.click();
button.click();
button.click();
}
Example 4: Sorting
struct Person {
string name;
int age;
};
int main() {
vector<Person> people = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
// Sort by age
sort(people.begin(), people.end(),
[](const Person& a, const Person& b) {
return a.age < b.age;
});
for (const auto& p : people) {
cout << p.name << ": " << p.age << endl;
}
// Bob: 25
// Alice: 30
// Charlie: 35
}
Capturing this
class Counter {
private:
int count = 0;
public:
auto getIncrementer() {
// Capture this (member access)
return [this]() {
return ++count;
};
}
auto getIncrementerCopy() {
// Copy of *this (C++17)
return [*this]() mutable {
return ++count; // modifies the copy’s members
};
}
int getCount() const {
return count;
}
};
int main() {
Counter counter;
auto inc = counter.getIncrementer();
cout << inc() << endl; // 1
cout << inc() << endl; // 2
cout << counter.getCount() << endl; // 2
}
The mutable keyword
Example C++:
int x = 10;
// By-value capture is const by default
auto f1 = [x]() {
// x++; // error: const
return x;
};
// mutable allows mutating the copy inside the lambda
auto f2 = [x]() mutable {
x++; // OK (copy)
return x;
};
cout << f2() << endl; // 11
cout << x << endl; // 10 (original unchanged)
Common problems
Problem 1: Dangling references
Example C++:
// Dangling reference
function<int()> makeFunc() {
int x = 10;
return [&x]() { return x; }; // x is destroyed when makeFunc returns
}
auto f = makeFunc();
// cout << f() << endl; // UB: x is gone
// By-value capture
function<int()> makeFunc() {
int x = 10;
return [x]() { return x; }; // copy survives
}
Problem 2: Missing capture
Example C++:
int x = 10;
int y = 20;
// y not captured
auto f = [x]() {
return x + y; // error: y not captured
};
// Capture y
auto f = [x, y]() {
return x + y;
};
// Or default capture
auto f = [=]() {
return x + y;
};
Problem 3: Lifetime of this
class Widget {
public:
auto getCallback() {
// Dangerous if Widget is destroyed before the lambda runs
return [this]() {
// UB if Widget is gone
};
}
// shared_ptr for extended lifetime
auto getCallback(shared_ptr<Widget> self) {
return [self]() {
// safer if you control lifetime via shared_ptr
};
}
};
Capture cheat sheet
Example C++:
[] // nothing captured
[x] // x by value
[&x] // x by reference
[=] // all used locals by value
[&] // all used locals by reference
[=, &x] // default by value, x by reference
[&, x] // default by reference, x by value
[this] // capture this pointer
[*this] // capture a copy of *this (C++17)
[x = 42] // init capture (C++14)
Production patterns
Pattern 1: Deferred work
class TaskScheduler {
std::vector<std::function<void()>> tasks_;
public:
void schedule(std::function<void()> task) {
tasks_.push_back(task);
}
void executeAll() {
for (auto& task : tasks_) {
task();
}
tasks_.clear();
}
};
TaskScheduler scheduler;
int x = 10;
scheduler.schedule([x]() {
std::cout << "Task 1: " << x << '\n';
});
scheduler.schedule([&x]() {
x++;
std::cout << "Task 2: " << x << '\n';
});
scheduler.executeAll();
Pattern 2: Callback chains
class AsyncOperation {
public:
template<typename F>
void then(F&& callback) {
std::thread([callback = std::forward<F>(callback)]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
callback();
}).detach();
}
};
AsyncOperation op;
int result = 0;
op.then([&result]() {
result = 42;
std::cout << "Done: " << result << '\n';
});
Pattern 3: State machine
class StateMachine {
std::function<void()> currentState_;
public:
void setState(std::function<void()> state) {
currentState_ = state;
}
void execute() {
if (currentState_) {
currentState_();
}
}
};
StateMachine sm;
int count = 0;
auto idle = [&]() {
std::cout << "Idle\n";
if (count++ > 3) {
sm.setState([&]() {
std::cout << "Active\n";
});
}
};
sm.setState(idle);
sm.execute();
FAQ
Q1: By value vs by reference?
A:
[x]by value: safer (uses a copy), costs a copy, does not change the original (unless you only mutate the copy withmutable).[&x]by reference: no copy, can mutate the original, dangling if the referent is destroyed.
Example C++:
int x = 10;
auto f1 = [x]() { return x; };
auto f2 = [&x]() { return x; };
Rule of thumb:
- If the lambda escapes the function (returned, stored, async): prefer by value.
- If it is used only locally with obvious lifetime: by reference is often fine.
Q2: When do I need mutable?
A: When you need to modify a by-value capture inside the lambda. Those members behave like const in a const operator() unless you add mutable.
Example C++:
int x = 10;
auto f1 = [x]() {
// x++; // error
};
auto f2 = [x]() mutable {
x++; // OK (copy)
return x;
};
std::cout << f2() << '\n'; // 11
std::cout << x << '\n'; // 10
Q3: [=] vs [&]?
A:
[=]: capture everything you use by value (safer, copy cost).[&]: capture everything you use by reference (fast, lifetime risk).
Example C++:
int x = 10, y = 20;
auto f1 = [=]() { return x + y; };
auto f2 = [&]() { return x + y; };
Practice: explicit lists like [x, &y] are usually clearer and easier to review.
Q4: When do I capture this?
A: In member functions when the lambda must use members of the current object. [this] stores the pointer; [*this] (C++17) stores a copy of the object.
class Counter {
int count_ = 0;
public:
auto getIncrementer() {
return [this]() {
return ++count_;
};
}
auto getIncrementerCopy() {
return [*this]() mutable {
return ++count_;
};
}
};
Q5: What is init capture?
A: C++14 lets you name captures with arbitrary expressions, including move into the closure.
Example C++:
auto f1 = [x = 42]() { return x; };
auto ptr = std::make_unique<int>(10);
auto f2 = [p = std::move(ptr)]() {
return *p;
};
int x = 10;
auto f3 = [y = x * 2]() { return y; };
Q6: Performance considerations?
A:
- By value: copy cost (large objects: consider
constreference capture with care, or move capture). - By reference: no copy (watch lifetimes).
- Move capture: transfer ownership without a deep copy (C++14).
Example C++:
std::vector<int> vec(1000000);
auto f1 = [vec]() { return vec.size(); };
auto f2 = [&vec]() { return vec.size(); };
auto f3 = [vec = std::move(vec)]() { return vec.size(); };
Q7: Further reading?
A:
- Effective Modern C++ (Items 31–34), Scott Meyers
- cppreference.com — Lambda expressions
- C++ Lambda Story, Bartłomiej Filipek
See also: Lambda complete guide, Init capture.
In one line: lambda capture binds outer names into the closure by value or reference so the lambda body can use them safely—if you respect object lifetimes.
Related posts
- C++ lambdas — complete guide
- C++ init capture
- C++ generic lambda