Master Lambda Expressions to Write Cleaner, More Expressive C++
Learn to write lambda expressions in C++ — from basic syntax to capture semantics — so you can pass behavior as values and eliminate boilerplate functors.
By the end of this page, you will be able to write lambda expressions from scratch, choose the right capture mode for your situation, use lambdas with standard algorithms, and avoid the most common lifetime and performance traps.
What and Why
Before lambdas, passing custom behavior to an algorithm required writing a separate functor class — a struct with an operator(). That works, but it forces you to name a concept that only exists to be used once, and to scatter that logic far from where it is called.
A lambda expression is an anonymous function you define right where you need it. The compiler generates the functor class for you, behind the scenes. The result is code that reads the way you think: action, then the detail that customises it.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9};
// Sort descending — the sorting rule lives right here.
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
for (int x : v) std::cout << x << ' '; // 9 5 4 3 1 1
}That [](int a, int b) { return a > b; } is a lambda. Every piece of it has a name.
Step by Step
The anatomy of a lambda
[ captures ] ( parameters ) -> return_type { body }Only the capture list [] and the body {} are required. Everything else is optional when the compiler can deduce it.
Minimal lambda
#include <iostream>
int main() {
auto greet = []() { std::cout << "Hello\n"; };
greet(); // prints: Hello
}auto is almost always the right type to use when storing a lambda — each lambda has a unique, unnamed type.
Parameters work like any function
#include <iostream>
int main() {
auto add = [](int a, int b) { return a + b; };
std::cout << add(2, 3); // 5
}The return type is deduced automatically when the body contains a single return statement. For more complex bodies, annotate explicitly:
auto divide = [](double a, double b) -> double {
if (b == 0.0) return 0.0;
return a / b;
};Capturing local variables
This is where lambdas become genuinely powerful. The capture list lets the lambda close over variables from the surrounding scope — hence the term closure.
Capture by value — a copy is made when the lambda is created:
#include <iostream>
int main() {
int threshold = 5;
auto above = [threshold](int x) { return x > threshold; };
threshold = 100; // too late — the lambda already has its own copy
std::cout << above(7); // 1 (true)
}Capture by reference — the lambda holds a reference to the original variable:
#include <iostream>
int main() {
int count = 0;
auto inc = [&count]() { ++count; };
inc(); inc(); inc();
std::cout << count; // 3
}Default captures let you avoid listing every variable:
| Syntax | Meaning |
|---|---|
[=] | Capture all used locals by value |
[&] | Capture all used locals by reference |
[=, &x] | Defaults to value, but x by reference |
[&, y] | Defaults to reference, but y by value |
Prefer explicit captures when possible — they make dependencies obvious at a glance.
Mutable lambdas (C++11)
Value-captured copies are const by default. Add mutable to allow modification:
#include <iostream>
int main() {
int x = 0;
auto counter = [x]() mutable {
++x; // modifies the lambda's own copy, not the outer x
return x;
};
std::cout << counter() << ' '; // 1
std::cout << counter() << ' '; // 2
std::cout << x; // 0 — outer x is unchanged
}Generic lambdas (C++14)
Use auto parameters to write lambdas that work for any type:
#include <iostream>
int main() {
auto print = [](auto val) { std::cout << val << '\n'; };
print(42);
print(3.14);
print("hello");
}The compiler generates a templated operator() — one instantiation per distinct type used.
Common Patterns
1. Inline predicates for standard algorithms
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
int main() {
std::vector<std::string> words = {"apple", "fig", "banana", "kiwi"};
// Keep only short words.
auto end = std::remove_if(words.begin(), words.end(),
[](const std::string& s) { return s.size() > 4; });
words.erase(end, words.end());
for (const auto& w : words) std::cout << w << ' '; // fig kiwi
}2. Callbacks and deferred work
#include <functional>
#include <iostream>
void run_after(int times, std::function<void(int)> action) {
for (int i = 0; i < times; ++i) action(i);
}
int main() {
int total = 0;
run_after(5, [&total](int i) { total += i; });
std::cout << total; // 0+1+2+3+4 = 10
}std::function erases the lambda's unique type so it can be stored or passed through interfaces. It has overhead — prefer auto or templates when you control both sides.
3. Immediately invoked lambdas for complex initialization
When initialising a const value that requires several steps, an immediately invoked lambda keeps the logic inline without sacrificing const:
#include <iostream>
#include <vector>
int main() {
const int result = [&]() {
int acc = 0;
for (int i = 1; i <= 10; ++i) acc += i;
return acc;
}(); // <-- called immediately
std::cout << result; // 55
}What Can Go Wrong
Dangling reference captures
Capturing by reference is safe only while the referred-to variable is alive. Return a lambda that holds a reference to a local variable and you have undefined behaviour:
// BROKEN
auto make_counter() {
int n = 0;
return [&n]() { return ++n; }; // n is destroyed when make_counter returns
}Fix: capture by value when the lambda outlives the surrounding scope.
auto make_counter() {
int n = 0;
return [n]() mutable { return ++n; }; // safe — owns its own copy
}Capturing this incorrectly
Inside a member function, [=] before C++20 captures this by pointer, not by value. If the object is destroyed before the lambda runs, you dereference a dangling pointer.
// C++17 and earlier — [=] silently captures this
auto bad = [=]() { return member_; }; // copies this pointer, not *this
// C++17: explicit value copy of the whole object
auto good = [*this]() { return member_; }; // safe copyOverusing std::function
std::function involves heap allocation and virtual dispatch. For a lambda used only in a tight loop, prefer a template parameter:
// Prefer this for performance-sensitive code:
template<typename F>
void apply(F func, int x) { func(x); }
// Over this:
void apply(std::function<void(int)> func, int x) { func(x); }Quick Reference
| Feature | Syntax | Standard |
|---|---|---|
| Basic lambda | [](int x) { return x * 2; } | C++11 |
| Capture by value | [x] or [=] | C++11 |
| Capture by reference | [&x] or [&] | C++11 |
| Mutable capture | [x]() mutable { ++x; } | C++11 |
| Explicit return type | []() -> int { ... } | C++11 |
| Generic lambda | [](auto x) { ... } | C++14 |
Copy of *this | [*this] | C++17 |
| Template lambda | []<typename T>(T x) { ... } | C++20 |
consteval lambda | []() consteval { ... } | C++20 |
What's Next
Lambdas interact with several powerful language features worth exploring next:
- Constant expressions and
constexprlambdas — lambdas can run at compile time since C++17 - Fold expressions — combine variadic packs alongside lambdas for concise generic algorithms
- Lambda reference — complete syntax reference with all capture forms
- Advanced lambda techniques — recursive lambdas, overload patterns, and stateful lambdas in depth