Lambda Expressions
Reviewed & published by Brayan K
A lambda is a tiny function you write right where you use it — no separate declaration, no name needed. They make standard algorithms like std::sort and std::count_if read like plain English, and they're the everyday glue of modern C++. By the end of this lesson you'll write lambdas with parameters and captures, and pass them to algorithms to filter, sort, and accumulate data.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Write inline functions with the [capture](params){ body } syntax
- Store a lambda in a variable with auto and call it
- Capture surrounding variables by value [x] and by reference [&x]
- Pass lambdas to std::sort, count_if, for_each, find_if
- Keep state across calls with mutable captures
- Avoid dangling-reference and capture-by-value pitfalls
💡 Real-World Analogy
A lambda is a sticky note with an instruction that you hand to a helper. When you ask std::sort to put a list in order, you can attach a sticky note that says "shorter words first" — that note is the lambda. The capture list is what you write on the back of the note from your own desk: copy a number to use ([value]), or point back at the original so changes are reflected ([&reference]). The helper doesn't need to know your whole program — just the small rule on the note.
📊 Anatomy of a Lambda
[capture](parameters) -> returnType { body }
| | | |
| | | +-- the code to run
| | +------------- optional; usually deduced
| +--------------------------- inputs, like any function
+-------------------------------------- which outside variables to use| Capture | Meaning |
|---|---|
| [] | capture nothing |
| [x] | capture x by value (a copy) |
| [&x] | capture x by reference (the original) |
| [=] | capture everything used, by value |
| [&] | capture everything used, by reference |
| [x, &y] | x by value, y by reference |
1. Your First Lambdas
The simplest lambda has an empty capture list [], no parameters, and a body. Store it in a variable with auto (each lambda has its own unique compiler-generated type) and call it like a function. Lambdas can take parameters and return values just like named functions — the return type is usually deduced automatically.
#include <iostream>
using namespace std;
int main() {
// A lambda is an unnamed function written inline.
// Shape: [capture](params) { body }
auto greet = []() {
cout << "Hello from a lambda!" << endl;
};
greet(); // call it like a normal function -> Hello from a lambda!
// Lambdas can take parameters and return a value:
auto add = [](int a, int b) {
return a + b; // return type is deduced (int here)
};
cout << "3 + 4 = " << add(3, 4) << endl; // 3 + 4 = 7
// You can even call a lambda immediately:
int sq = [](int n) { return n * n; }(5);
cout << "5 squared = " << sq << endl; // 5 squared = 25
return 0;
}
// ✅ Expected output:
// Hello from a lambda!
// 3 + 4 = 7
// 5 squared = 252. Captures: by Value vs by Reference
A lambda can use variables from the surrounding scope, but only if you capture them. [factor] takes a copy — a snapshot frozen at the moment the lambda is created. [&total] takes a reference to the original, so the lambda can read and modify the live value. Choosing the right kind of capture is the heart of using lambdas well.
#include <iostream>
using namespace std;
int main() {
int factor = 10;
int total = 0;
// [factor] captures factor BY VALUE -> the lambda gets a copy.
auto scale = [factor](int x) { return x * factor; };
cout << scale(3) << endl; // 30
// Changing factor AFTER capture does NOT affect the copy:
factor = 99;
cout << scale(3) << endl; // still 30 (used the old copy)
// [&total] captures BY REFERENCE -> it touches the original.
auto addToTotal = [&total](int x) { total += x; };
addToTotal(5);
addToTotal(7);
cout << "total = " << total << endl; // total = 12
return 0;
}
// ✅ Expected output:
// 30
// 30
// total = 12Your turn. Fill in the capture so the lambda can use the bonus variable by value:
#include <iostream>
using namespace std;
int main() {
// 🎯 YOUR TURN — make a lambda that adds a fixed bonus.
int bonus = 50;
// Capture 'bonus' BY VALUE, take a score, return score + bonus.
auto withBonus = ___ (int score) { return score + bonus; };
// ^^^ 👉 [bonus]
cout << withBonus(100) << endl; // ✅ Expected: 150
cout << withBonus(20) << endl; // ✅ Expected: 70
return 0;
}🧩 Reorder Challenge
These lines define a lambda that doubles a number and use it. Put them in the correct order:
A. cout << twice(21) << endl;
B. auto twice = [](int n) { return n * 2; };
C. int main() {
D. return 0;
E. }Correct order: C → B → A → D → E
Open main (C), define the twice lambda (B), call it — printing 42 (A), then return 0; (D) and close the brace (E). A lambda must be defined before it's called.
3. Lambdas with std::sort
The most common use of a lambda is as a comparator for std::sort. You pass a lambda taking two elements that returns true when the first should come before the second. This lets you sort by any rule you like — by length, by a struct field, descending, whatever — without writing a separate function.
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
using namespace std;
int main() {
vector<string> names = {"Charlie", "Al", "Bob", "Dan"};
// Sort by LENGTH using a lambda as the comparator.
// It must return true when 'a' should come BEFORE 'b'.
sort(names.begin(), names.end(),
[](const string& a, const string& b) {
return a.size() < b.size(); // shorter strings first
});
for (const string& n : names) cout << n << " ";
cout << endl; // Al Bob Dan Charlie
return 0;
}
// ✅ Expected output:
// Al Bob Dan Charlie4. count_if, for_each, find_if
Lambdas shine with the standard algorithms. A lambda returning bool is called a predicate: count_if counts how many elements satisfy it, find_if returns the first that does. for_each runs an action on every element — capture a variable by reference to accumulate a result.
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {4, 7, 2, 9, 5, 1, 8};
// count_if: how many satisfy a predicate (a lambda returning bool)?
int evens = count_if(nums.begin(), nums.end(),
[](int n) { return n % 2 == 0; });
cout << "even count: " << evens << endl; // even count: 3
// for_each: run an action on every element.
int sum = 0;
for_each(nums.begin(), nums.end(),
[&sum](int n) { sum += n; });
cout << "sum: " << sum << endl; // sum: 36
// find_if: first element matching a rule (returns an iterator).
auto it = find_if(nums.begin(), nums.end(),
[](int n) { return n > 6; });
if (it != nums.end()) cout << "first >6: " << *it << endl; // first >6: 7
return 0;
}
// ✅ Expected output:
// even count: 3
// sum: 36
// first >6: 75. mutable: Lambdas That Remember
By default, variables captured by value are const inside the lambda — the body can read its copies but not change them. Add the mutable keyword after the parameter list to allow changes to those copies. Combined with an init-capture like [count = 0], this lets a lambda keep its own state between calls, behaving like a tiny stateful object.
#include <iostream>
using namespace std;
int main() {
// A lambda can keep STATE across calls by capturing by value
// and marking itself 'mutable' so it may change its own copy.
auto counter = [count = 0]() mutable {
return ++count; // the captured copy persists between calls
};
cout << counter() << endl; // 1
cout << counter() << endl; // 2
cout << counter() << endl; // 3
// Without 'mutable', the body could not modify 'count' (it's const).
return 0;
}
// ✅ Expected output:
// 1
// 2
// 3🔎 Deep Dive: what a lambda really is
A lambda is syntactic sugar for an unnamed function object (a "functor"): the compiler generates a hidden struct with an operator(), and the captures become its data members. That's why each lambda has a unique type and why auto is the natural way to hold one.
When you need a single named type that can hold any callable with a given signature — for example to store different lambdas in one container — use std::function, at a small runtime cost:
#include <functional>
std::function<int(int)> op = [](int x) { return x + 1; };
op = [](int x) { return x * x; }; // reassign to a different lambda
cout << op(5); // 25Pro Tips
- 💡 Capture what you need: prefer [x, &y] over blanket [=] / [&] so intent is explicit.
- 💡 Use auto to store lambdas: their types are unnameable; auto just works.
- 💡 Reach for predicates: count_if, find_if, all_of with a lambda replace many hand-written loops.
- 💡 Beware reference captures that outlive their target: a [&] capture of a local becomes dangling once the local dies.
🧠 Quick Recall
Predict the output before revealing the answer.
int k = 5;
auto f = [k](int n) { return n + k; };
k = 100;
cout << f(1);6 — k was captured by value (a copy of 5). Changing k to 100 afterward doesn't affect the lambda's copy: 1 + 5 = 6.
2. What is the final value of total?
int total = 0;
auto add = [&total](int n) { total += n; };
add(3); add(4);
cout << total;7 — total is captured by reference, so both calls modify the original: 0 + 3 + 4 = 7.
3. What does this print?
auto next = [i = 0]() mutable { return i++; };
cout << next() << next() << next();012 — post-increment returns the old value then increments the captured copy: 0, then 1, then 2.
Common Errors (and the fix)
- Using a variable you didn't capture: "error: 'x' is not captured" means you must add it to the capture list, e.g. [x] or [&x].
- Modifying a value capture without mutable: [n](){ n++; } fails because the copy is const. Add mutable after the parameter list.
- Dangling reference capture: returning a lambda that captures a local by [&] leaves it pointing at a destroyed variable — capture by value instead.
- Comparator that returns <=: a std::sort comparator must be a strict less-than (return a < b, never a <= b), or you get undefined behaviour.
- Forgetting #include <algorithm>: sort, count_if, find_if, and for_each all live there.
📋 Quick Reference
| Task | Code |
|---|---|
| No-capture lambda | auto f = [](int x){ return x+1; }; |
| Capture by value | [k](int x){ return x*k; } |
| Capture by reference | [&sum](int x){ sum += x; } |
| Mutable / stateful | [c=0]() mutable { return ++c; } |
| Sort with comparator | sort(b, e, [](auto a, auto b){...}) |
| Count matching | count_if(b, e, pred) |
| Store any callable | function<int(int)> g = ...; |
Mini-Challenge: Filter and Sum
Combine a predicate and a reference capture: count the expensive items and sum only those. The comment outline keeps you on track.
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
// 🎯 MINI-CHALLENGE: Filter and sum
// Given the prices below:
// 1. Use count_if with a lambda to count how many prices are > 20.
// 2. Use for_each (or a loop) with a lambda capturing a 'total'
// by reference to sum ONLY the prices > 20.
// 3. Print the count and the total.
//
vector<double> prices = {12.5, 30.0, 8.0, 45.5, 22.0, 5.0};
//
// ✅ Example output:
// expensive items: 3
// expensive total: 97.5
// your code here
return 0;
}🎉 Lesson Complete
- ✅ A lambda is an inline unnamed function: [capture](params){ body }
- ✅ Store one with auto; capture by value [x] or by reference [&x]
- ✅ Value captures freeze a snapshot; reference captures stay live
- ✅ Pass lambdas to sort, count_if, for_each, find_if
- ✅ mutable + init-capture lets a lambda keep state across calls
Practice quiz
What is the basic syntax of a C++ lambda expression?
- [capture](parameters) { body }
- lambda(parameters) -> body
- function[capture](parameters)
- { body }(parameters)[capture]
Answer: [capture](parameters) { body }. A lambda is written [capture](parameters) { body }, with an optional -> returnType after the parameters.
What does the capture [x] do?
- Captures x by reference
- Captures x by value (a copy)
- Captures everything by value
- Captures nothing
Answer: Captures x by value (a copy). [x] captures x by value — the lambda gets its own copy, frozen at creation time. [&x] would capture by reference.
What prints? int k = 5; auto f = [k](int n){ return n + k; }; k = 100; cout << f(1);
- 101
- 6
- 105
- 1
Answer: 6. k was captured by value (a copy of 5). Changing k to 100 afterward does not affect the copy, so f(1) = 1 + 5 = 6.
What is the final value of total? int total = 0; auto add = [&total](int n){ total += n; }; add(3); add(4);
- 0
- 3
- 7
- 12
Answer: 7. total is captured by reference, so both calls modify the original: 0 + 3 + 4 = 7.
By default, can a lambda modify a variable it captured by value inside its body?
- Yes, always
- No — the copy is const unless you add mutable
- Only if it is an int
- Only with [&] capture
Answer: No — the copy is const unless you add mutable. Value captures are const inside the body by default. Adding mutable after the parameter list lets the lambda change its own copies.
Which keyword is the natural way to store a lambda in a variable?
- auto
- lambda
- var
- void
Answer: auto. Each lambda has its own unique compiler-generated type, so auto is the natural way to hold one.
A lambda passed to std::sort as a comparator should return true when:
- The two elements are equal
- The first element should come before the second
- The first element is greater
- Sorting is finished
Answer: The first element should come before the second. A sort comparator must be a strict less-than: return true when 'a' should come before 'b' (e.g. a.size() < b.size()).
What does std::count_if(begin, end, pred) return?
- An iterator to the first match
- true or false
- How many elements satisfy the predicate
- The sum of matching elements
Answer: How many elements satisfy the predicate. count_if counts how many elements satisfy the predicate. find_if returns an iterator to the first match; for_each runs an action on each.
What is a lambda really, under the hood?
- A macro
- An unnamed function object (functor) with operator() and captures as data members
- A plain function pointer
- A preprocessor directive
Answer: An unnamed function object (functor) with operator() and captures as data members. The compiler generates a hidden struct with operator(); the captures become its data members — which is why each lambda has a unique type.
To store any callable with signature int(int) in one named type, you would use:
- std::function<int(int)>
- auto
- int*
- std::vector<int>
Answer: std::function<int(int)>. std::function<int(int)> can hold any callable matching that signature (at a small runtime cost), letting you reassign different lambdas.
Continue this course
- Previous: Recursion
- Next: Arrays & Vectors — Store collections of data with C arrays and the modern std::vector
- Quick reference: C++ cheat sheet › Modern C++
Frequently asked questions
What is a lambda expression in C++?
A lambda is a small, unnamed function you can write inline, right where you need it. The basic shape is [capture](parameters) { body }. They are most often passed to algorithms like std::sort, std::for_each, or std::count_if to describe a custom rule.
What does the [ ] capture list do?
It controls which surrounding variables the lambda can see and how. [] captures nothing, [x] captures x by value (a copy), [&x] captures x by reference (the original), [=] captures everything used by value, and [&] captures everything by reference.
What is the difference between [=] and [&]?
[=] copies the variables it uses, so the lambda has its own snapshot that won't change if the originals do. [&] refers to the originals, so it sees and can modify the current values. Prefer capturing exactly what you need rather than [=] or [&] blanket captures.
Why would I use mutable on a lambda?
By default a lambda's body cannot modify variables captured by value (the copies are const). Adding the mutable keyword after the parameter list lets the body change those copies. The originals outside are still untouched, because only the copies change.
How do I store a lambda in a variable?
Use auto to let the compiler deduce the lambda's unique type: auto add = [](int a, int b){ return a + b; };. If you need a named, assignable function type you can store it in std::function<int(int,int)>, at a small runtime cost.