[2026] C++ Lambda Capture — Value vs Reference, init capture, `this`, and pitfalls
이 글의 핵심
Lambda capture defines how a lambda accesses variables from its enclosing scope. You can capture by copy or by reference; the choice affects safety, performance, and whether updates are visible outside the lambda.
What is lambda capture?
Lambda capture defines how a lambda accesses variables from its enclosing scope. A lambda can capture names from the scope where it is defined; depending on the capture form, it holds a copy or a reference to each entity.
Example:
int x = 10;
// Value capture
auto f1 = [x]() { return x; };
// Reference capture
auto f2 = [&x]() { return x; };
// Default: capture everything by value
auto f3 = [=]() { return x; };
// Default: capture everything by reference
auto f4 = [&]() { return x; };
Why capture exists
- Closures: the lambda can “remember” outer state.
- Flexibility: choose copy vs reference per variable.
- Conciseness: less boilerplate than a hand-written functor.
- Type safety: the compiler checks what you capture.
// Functor: more 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: shorter
int x = 10;
auto add10 = [x](int y) { return x + y; };
std::cout << add10(5) << '\n'; // 15
How it is implemented
A lambda is lowered to an anonymous function object. Captured variables become data members of that type.
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);
Value capture vs reference capture
int x = 10;
// Value capture: a copy inside the closure
auto f1 = [x]() mutable {
x++; // modifies the copy
return x;
};
std::cout << f1() << std::endl; // 11
std::cout << x << std::endl; // 10 (original unchanged)
// Reference capture: refers to the original
auto f2 = [&x]() {
x++; // modifies the original
return x;
};
std::cout << f2() << std::endl; // 11
std::cout << x << std::endl; // 11 (original changed)
Mixed capture
int x = 10;
int y = 20;
// x by value, y by reference
auto f = [x, &y]() {
// x++; // error: copy is const unless mutable
y++; // OK: reference capture
return x + y;
};
std::cout << f() << std::endl; // 31
std::cout << x << std::endl; // 10
std::cout << y << std::endl; // 21
Init capture (C++14)
// Introduce a new member with an initializer
auto f1 = [x = 42]() {
return x;
};
// Move capture
auto ptr = std::make_unique<int>(10);
auto f2 = [p = std::move(ptr)]() {
return *p;
};
// Expression capture
int x = 10;
auto f3 = [y = x * 2]() {
return y;
};
std::cout << f3() << std::endl; // 20
Practical examples
Example 1: counter
makeCounter returns a lambda that holds its own counter state.
auto makeCounter() {
int count = 0;
return [count]() mutable {
return ++count;
};
}
int main() {
auto counter = makeCounter();
std::cout << counter() << std::endl; // 1
std::cout << counter() << std::endl; // 2
std::cout << counter() << std::endl; // 3
}
Example 2: filter
std::vector<int> filterGreaterThan(const std::vector<int>& vec, int threshold) {
std::vector<int> result;
std::copy_if(vec.begin(), vec.end(), std::back_inserter(result),
[threshold](int x) {
return x > threshold;
});
return result;
}
int main() {
std::vector<int> nums = {1, 5, 3, 8, 2, 9, 4};
auto filtered = filterGreaterThan(nums, 5);
for (int n : filtered) {
std::cout << n << " ";
}
std::cout << std::endl; // 8 9
}
Example 3: event handler
class Button {
private:
std::function<void()> onClick;
public:
void setOnClick(std::function<void()> handler) {
onClick = handler;
}
void click() {
if (onClick) {
onClick();
}
}
};
int main() {
Button button;
int clickCount = 0;
// Reference capture for clickCount
button.setOnClick([&clickCount]() {
clickCount++;
std::cout << "Click " << clickCount << '\n';
});
button.click();
button.click();
button.click();
}
Example 4: sorting
struct Person {
std::string name;
int age;
};
int main() {
std::vector<Person> people = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
std::sort(people.begin(), people.end(),
[](const Person& a, const Person& b) {
return a.age < b.age;
});
for (const auto& p : people) {
std::cout << p.name << ": " << p.age << std::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 *this (C++17)
return [*this]() mutable {
return ++count; // modifies the copy’s view of members
};
}
int getCount() const {
return count;
}
};
int main() {
Counter counter;
auto inc = counter.getIncrementer();
std::cout << inc() << std::endl; // 1
std::cout << inc() << std::endl; // 2
std::cout << counter.getCount() << std::endl; // 2
}
The mutable keyword
int x = 10;
// Copy capture: members are const in operator() by default
auto f1 = [x]() {
// x++; // error: const
return x;
};
// mutable: can modify the captured copy
auto f2 = [x]() mutable {
x++; // OK (copy)
return x;
};
std::cout << f2() << std::endl; // 11
std::cout << x << std::endl; // 10 (original unchanged)
Common problems
Problem 1: dangling reference
// Bad: dangling reference
std::function<int()> makeFunc() {
int x = 10;
return [&x]() { return x; }; // x is destroyed when makeFunc returns
}
auto f = makeFunc();
// std::cout << f() << std::endl; // UB: x is gone
// Good: value capture
std::function<int()> makeFuncOk() {
int x = 10;
return [x]() { return x; };
}
Problem 2: missing capture
int x = 10;
int y = 20;
// Bad: y not captured
// auto f = [x]() {
// return x + y; // error: y not captured
// };
// Good: capture both
auto f = [x, y]() {
return x + y;
};
// Or default capture
auto g = [=]() {
return x + y;
};
Problem 3: this lifetime
class Widget {
public:
auto getCallback() {
// Dangerous if Widget is destroyed before the lambda runs
return [this]() {
// UB if *this is gone
};
}
// Safer: shared ownership
auto getCallback(std::shared_ptr<Widget> self) {
return [self]() {
(void)self;
};
}
};
Capture cheat sheet
[] // no capture
[x] // x by value
[&x] // x by reference
[=] // default: all automatic variables by value
[&] // default: all automatic variables by reference
[=, &x] // x by reference, others by value
[&, x] // x by value, others by reference
[this] // capture this pointer
[*this] // copy the object (C++17)
[x = 42] // init capture (C++14)
Production-oriented patterns
Pattern 1: deferred execution
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 chain
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: simple 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: Value capture vs reference capture?
A
[x](by value): safer (own copy), copy cost, does not mutate the original.[&x](by reference): no copy, risk of dangling references, can mutate the original.
int x = 10;
auto f1 = [x]() { return x; };
auto f2 = [&x]() { return x; };
Rule of thumb
- If the lambda escapes the function (returned, stored): prefer value (or clear ownership such as
shared_ptr). - If the lambda is used only locally while everything is alive: reference can be fine.
Q2: When do I need mutable?
A When you need to modify copy-captured members inside operator(). Copies are const by default unless the lambda is mutable.
int x = 10;
// auto f1 = [x]() { x++; }; // error
auto f2 = [x]() mutable {
x++;
return x;
};
std::cout << f2() << '\n'; // 11
std::cout << x << '\n'; // 10
Q3: [=] vs [&]?
A
[=]: capture all automatic variables by value (safer, may copy a lot).[&]: capture all by reference (fast, easy to create dangling references).
int x = 10, y = 20;
auto f1 = [=]() { return x + y; };
auto f2 = [&]() { return x + y; };
In real code, explicit capture such as [x, &y] is often clearer and safer.
Q4: When do I use this capture?
A Inside member functions when the lambda must use members of *this. [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 A C++14 feature: introduce a new closure member with = or move from an existing object.
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
- Value capture: copying large objects can be expensive.
- Reference capture: no copy, but lifetime must outlive all uses.
- Move init capture: transfer ownership without a deep copy (C++14).
std::vector<int> vec(1000000);
// auto f1 = [vec]() { return vec.size(); }; // large copy
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
Related posts: Lambda complete, Init capture.
One-line summary: Lambda capture lets a closure hold outer names by value or reference so the body can use them safely—or unsafely if lifetimes are wrong.
See also
- C++ lambdas — anonymous functions, capture,
mutable - C++ init capture
- C++ lambda basics — capture,
mutable, generic lambdas
Related posts
- C++ lambda capture mistakes
- C++ lambdas — overview
std::functionvs function pointers- C++
constexprlambda - C++ generic lambda