C++ Lambda Expressions and Captures
Master C++ lambdas: syntax, capture modes by value and reference, mutable lambdas, generic lambdas, and lifetime pitfalls.
What you'll learn
- ✓Lambda syntax
- ✓Capture by value vs reference
- ✓Mutable lambdas
- ✓Generic lambdas
- ✓Lifetime traps
Prerequisites
- •Basic familiarity with C++
What and Why
C++11 introduced lambdas to give you function objects without the boilerplate of writing a class with operator(). They are essential for STL algorithms, custom comparators, callbacks, and any code where you want to pass behavior around.
A lambda is just sugar for an anonymous class with a call operator and (optionally) captured state.
Mental Model
The compiler rewrites [x](int y) { return x + y; } into roughly this:
struct __Lambda12 {
int x;
int operator()(int y) const { return x + y; }
};
Captures become member variables. The mutable keyword removes the const on operator(). Generic lambdas (auto parameters) become a templated operator().
[ ] no captures
[=] capture all used variables by value (copy)
[&] capture all used variables by reference
[x, &y] x by value, y by reference
[=, &z] everything by value, z by reference
[this] capture enclosing object pointer
[*this] capture a copy of the enclosing object (C++17) Hands-on Example
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums = {5, 2, 8, 1, 9, 3};
int threshold = 4;
// Count by value capture
auto count = std::count_if(nums.begin(), nums.end(),
[threshold](int n) { return n > threshold; });
// Mutable lambda accumulating state
auto running_sum = [sum = 0](int n) mutable {
sum += n;
return sum;
};
for (int n : nums) std::cout << running_sum(n) << " ";
// Generic lambda (C++14)
auto print = [](const auto& x) { std::cout << x << "\n"; };
print(42);
print("hello");
}
The [sum = 0] is an init-capture (C++14): you create a new variable scoped to the lambda.
Common Pitfalls
Dangling references. [&] captures by reference, so storing the lambda beyond the scope of the captured variable is undefined behavior:
std::function<int()> make_bad() {
int x = 10;
return [&]() { return x; }; // x dies, lambda holds dangling ref
}
Capture by value when the lambda outlives the enclosing scope.
Capturing this accidentally. Inside a member function, [=] historically captured this by pointer (giving you access to all members). If the object dies before the lambda runs, you have undefined behavior. Since C++20, [=, this] is required to be explicit. Use [*this] (C++17) to capture a copy of the whole object.
Reference captures of loop variables. Spawning lambdas in a loop with [&i] and storing them leaves them all referring to the same i.
Large captures. Each by-value capture is a copy. Capturing a 1MB std::vector by value into a lambda passed to std::async makes a copy every call. Capture by reference (carefully) or move with [v = std::move(v)].
Practical Tips
- Default to
[=]for short-lived callbacks; default to explicit captures[x, &y]for long-lived ones. - Use init-captures to move-only types into a lambda:
[p = std::move(uniquePtr)]() { ... }. - For STL algorithms with simple predicates, prefer named lambdas stored in a
const autofor readability. constexprlambdas (C++17) let you use them inconstexprcontexts.- C++20 templated lambdas (
[]<typename T>(T x) { ... }) give you typename access withoutdecltype.
Wrap-up
C++ lambdas are powerful but unforgiving about lifetimes. Always ask: what does this lambda capture, and will those things still be alive when the lambda runs? Once that becomes second nature, lambdas turn STL algorithms, ranges, and async code into clean, expressive C++ instead of verbose functor classes.
Related articles
- C++ C++ Memory Model and Atomics
Understand the C++ memory model, memory orderings, and how std::atomic enables correct lock-free programming across threads.
- C++ C++ CMake Tutorial: Build Your First Project
Learn modern CMake for C++ — targets, properties, and dependencies — and configure a small project that compiles cleanly across platforms.
- C++ C++ constexpr and Compile-Time Computing
How constexpr, consteval, and constinit let you move computation from runtime to compile time, with practical patterns and the rules that govern them.
- C++ C++ Coroutines: An Introduction
Understand C++20 coroutines — co_await, co_yield, co_return — and how they enable async and generator-style code without blocking threads.