C++ Lambda Expressions
A lambda expression in C++ is a way to define a small, unnamed function right at the place where you need it, instead of writing a separate named function somewhere else in your file. Lambdas were introduced in C++11 and are used everywhere in modern C++ — as comparators for sorting, as callbacks, and as short one-off pieces of logic passed into algorithms. Once you understand how they capture variables from their surrounding scope, they become one of the most useful tools in the language.
Overview / How Lambda Expressions Work
Before lambdas existed, if you wanted a custom comparison function for std::sort, you had to write a separate named function (or a whole functor class) above main(), even if you only used it once. A lambda lets you write that logic inline, exactly where it is used, which keeps related code together and avoids cluttering your program with tiny one-use functions.
Under the hood, a lambda expression is not magic — the compiler translates it into an anonymous class (sometimes called a closure type) with an overloaded operator(). When you write [x](int y) { return x + y; }, the compiler generates a small class that stores a copy of x as a member variable and defines operator()(int y) to compute x + y. The lambda expression itself is really just the syntax for creating an instance of that compiler-generated class. This is why lambdas can “remember” variables from the scope they were created in — those variables become member data of the closure object.
The type of a lambda is unique and unnamed, which is why you almost always store it using auto, or pass it directly as an argument to a function template (like std::sort) that can deduce its type. If you need to store a lambda in a variable whose type must be written out explicitly (for example, a class member or a function return type), you can use std::function from the <functional> header, though this adds a small amount of runtime overhead compared to auto.
Syntax
The general form of a lambda expression is:
[capture-clause](parameters) -> returnType {
// body
};
| Part | Meaning |
|---|---|
[capture-clause] |
Which outside variables the lambda can access, and how (by value or by reference). |
(parameters) |
Just like a normal function’s parameter list. Can be empty: (). |
-> returnType |
Optional. The compiler can usually deduce the return type automatically from a single return statement, so this is only needed for more complex bodies. |
{ body } |
The statements the lambda executes, just like a regular function body. |
The capture clause is the part that is unique to lambdas. Common forms:
| Capture | Effect |
|---|---|
[] |
Captures nothing — the lambda can only use its own parameters and globals. |
[x] |
Captures x by value (a copy is made at the moment the lambda is created). |
[&x] |
Captures x by reference (changes inside the lambda affect the original x, and vice versa). |
[=] |
Captures every used outside variable by value automatically. |
[&] |
Captures every used outside variable by reference automatically. |
[=, &count] |
Captures everything by value, except count, which is captured by reference. |
[this] |
Captures the enclosing object by pointer, so the lambda can use its member variables and functions. |
Examples
Example 1: A basic lambda
#include <iostream>
int main() {
auto add = [](int a, int b) {
return a + b;
};
std::cout << "3 + 4 = " << add(3, 4) << std::endl;
std::cout << "10 + 20 = " << add(10, 20) << std::endl;
return 0;
}
Output:
3 + 4 = 7
10 + 20 = 30
Here add is a variable that holds a lambda, and it is called with parentheses just like a normal function. The empty [] capture clause means the lambda doesn’t need anything from the surrounding scope — it only uses its own parameters a and b.
Example 2: Capturing by value vs. by reference
#include <iostream>
int main() {
int multiplier = 3;
int counter = 0;
auto byValue = [multiplier](int x) {
return x * multiplier;
};
auto byReference = [&counter]() {
counter++;
};
std::cout << byValue(5) << std::endl;
multiplier = 100; // does not affect byValue anymore
std::cout << byValue(5) << std::endl;
byReference();
byReference();
byReference();
std::cout << "counter = " << counter << std::endl;
return 0;
}
Output:
15
15
counter = 3
Because multiplier is captured by value, byValue keeps its own private copy taken at the moment the lambda was created — changing multiplier afterward has no effect on it. In contrast, counter is captured by reference, so every call to byReference() directly modifies the original variable in main.
Example 3: Lambdas with algorithms
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
struct Student {
std::string name;
int score;
};
int main() {
std::vector<Student> students = {
{"Ana", 82},
{"Ben", 55},
{"Cleo", 91},
{"Dan", 47},
{"Eve", 68}
};
std::sort(students.begin(), students.end(), [](const Student& a, const Student& b) {
return a.score > b.score;
});
std::cout << "Ranked students:" << std::endl;
for (const Student& s : students) {
std::cout << s.name << ": " << s.score << std::endl;
}
int passMark = 60;
int passCount = std::count_if(students.begin(), students.end(), [passMark](const Student& s) {
return s.score >= passMark;
});
std::cout << "Students who passed (>= " << passMark << "): " << passCount << std::endl;
return 0;
}
Output:
Ranked students:
Cleo: 91
Ana: 82
Eve: 68
Ben: 55
Dan: 47
Students who passed (>= 60): 3
This is the most common real-world use of lambdas: passing custom logic into a standard library algorithm. std::sort uses the lambda to decide the order of two elements, and std::count_if uses a lambda that captures passMark by value to decide which elements satisfy a condition. Without lambdas you would need to write and name a separate comparator function for every different sort order you wanted.
How It Works Step by Step (Under the Hood)
When the compiler encounters a lambda expression, it performs roughly these steps:
- It generates a unique, unnamed class type (the closure type) with one member variable for each captured variable.
- Variables captured by value become copies stored as members; variables captured by reference become reference members pointing at the originals.
- The lambda’s body becomes the body of an
operator()member function on that class. - Writing the lambda expression
[...](...) { ... }creates (constructs) one object of that class — this object is called the closure. - Calling the lambda with
()is really just callingoperator()on that closure object.
Because captured-by-value members are stored as regular data members, by default operator() is treated as a const member function — you cannot modify a captured-by-value variable inside the lambda body. Adding the mutable keyword after the parameter list removes that restriction, allowing the lambda to modify its own internal copies (though this never affects the original variable outside the lambda):
#include <iostream>
int main() {
int count = 0;
auto increment = [count]() mutable {
count++;
std::cout << "Inside lambda: " << count << std::endl;
};
increment();
increment();
std::cout << "Outside lambda: " << count << std::endl;
return 0;
}
Output:
Inside lambda: 1
Inside lambda: 2
Outside lambda: 0
Notice that the outer count stays at 0 — mutable only lets the lambda change its own private copy, it does not turn the capture into a reference.
Since C++14, lambda parameters can also be declared with auto, creating a generic lambda that works with any type, similar to a function template:
#include <iostream>
int main() {
auto printDouble = [](auto value) {
std::cout << value << " doubled is " << value * 2 << std::endl;
};
printDouble(21);
printDouble(3.5);
return 0;
}
Output:
21 doubled is 42
3.5 doubled is 7
The compiler generates a separate operator() template instantiation for each type it is called with, exactly like it would for a function template.
Common Mistakes
Mistake 1: Returning a lambda that captured a local variable by reference
It’s tempting to capture by reference for efficiency, but if the lambda outlives the variable it references, you get a dangling reference and undefined behavior:
#include <functional>
std::function<int()> makeCounter() {
int count = 0;
auto increment = [&count]() {
return ++count;
};
return increment; // count goes out of scope here -- dangling reference!
}
Here count is a local variable of makeCounter. Once the function returns, count is destroyed, but the returned lambda still holds a reference to it — calling the returned lambda later reads memory that no longer belongs to a valid int.
The fix is to capture by value instead (combined with mutable so the lambda can update its own copy each time it’s called):
#include <iostream>
#include <functional>
std::function<int()> makeCounter() {
int count = 0;
auto increment = [count]() mutable {
return ++count;
};
return increment;
}
int main() {
auto counter1 = makeCounter();
std::cout << counter1() << std::endl;
std::cout << counter1() << std::endl;
std::cout << counter1() << std::endl;
return 0;
}
Output:
1
2
3
Now each returned lambda owns its own independent copy of count, so it stays valid no matter how long the lambda lives.
Mistake 2: Forgetting that captured-by-value variables are read-only by default
This code will not compile:
int total = 0;
auto addToTotal = [total](int x) {
total += x; // error: total is captured by value and is read-only here
return total;
};
Since total is captured by value and operator() is const by default, the compiler rejects the attempt to modify it. Beginners often expect capture-by-value to behave like a normal local variable that can be freely reassigned — it can’t, unless you add mutable. If you actually want the lambda to update the original variable, capture by reference instead:
#include <iostream>
int main() {
int total = 0;
auto addToTotal = [&total](int x) {
total += x;
return total;
};
std::cout << addToTotal(5) << std::endl;
std::cout << addToTotal(10) << std::endl;
std::cout << "Final total: " << total << std::endl;
return 0;
}
Output:
5
15
Final total: 15
Best Practices
- Prefer explicit captures like
[x]or[&x]over the blanket[=]or[&]— being explicit makes it obvious what the lambda depends on and helps avoid accidental dangling references. - Never capture by reference a local variable that might not exist anymore when the lambda is called (for example, a lambda returned from a function or stored for later use).
- Use
autoto store lambdas whenever possible; reservestd::functionfor cases where you truly need type erasure (like storing different lambdas in the same container or class member). - Keep lambda bodies short. If a lambda grows beyond a few lines, consider turning it into a named function for readability.
- Only add
mutablewhen you actually need to modify a captured-by-value variable inside the lambda — it’s easy to forget it only affects the lambda’s own copy. - Take capture-by-const-reference (
const auto&) for parameters of algorithm lambdas that receive large objects, to avoid unnecessary copies, just like you would with a normal function.
Practice Exercises
- Write a lambda that takes two
doubleparameters and returns their average. Store it withautoand call it with at least two different pairs of numbers. - Given a
std::vector<int>of exam scores, usestd::count_ifwith a lambda to count how many scores are below 50 (a “fail” count). - Write a lambda that captures an
intnamedtotalby reference and adds each argument it receives tototal. Call it three times with different numbers, then print the final value oftotalto confirm the reference capture worked.
Summary
- A lambda expression is inline, unnamed function syntax:
[capture](parameters) { body }. - The compiler turns each lambda into an anonymous class (a closure type) with an
operator(); captured variables become that class’s member data. - Capturing by value (
[x]) copies the variable at creation time; capturing by reference ([&x]) links directly to the original variable. - Lambdas are read-only over their by-value captures unless marked
mutable. - Generic lambdas (C++14) use
autoparameters to work with multiple types, like a function template. - Lambdas are most commonly used as inline comparators or predicates passed to standard library algorithms such as
std::sortandstd::count_if. - Never let a lambda hold a reference to a variable that could be destroyed before the lambda is called.
