Question
What is a C++ lambda expression? Why does C++ include lambda expressions, what problems do they solve that were difficult or awkward before they were added, and how can a developer benefit from using them?
Please include one or two examples, especially showing when and where lambda expressions are appropriate.
Short Answer
By the end of this page, you will understand what a C++ lambda expression is, why it was added to the language, and when it is a better choice than a regular function or function object. You will also learn how capture lists work, how lambdas are commonly used with standard library algorithms, and what mistakes beginners often make when using them.
Concept
A lambda expression in C++ is a way to create a small anonymous function object directly where you need it.
In simple terms, a lambda lets you write function-like behavior inline instead of defining a separate named function or custom class.
Basic example:
auto add = [](int a, int b) {
return a + b;
};
Here, add is a callable object created from a lambda.
Why C++ added lambdas
Before lambdas, if you wanted to pass custom behavior to algorithms like std::sort, std::find_if, or std::for_each, you usually had to use one of these:
- a regular function
- a pointer to a function
- a custom functor class with
operator()
Those approaches worked, but they had limitations:
- Regular functions cannot easily use local variables from the surrounding scope.
- Function pointers are limited and less expressive.
- Functor classes are powerful, but often verbose for simple one-time logic.
Lambdas solve this by letting you:
- define behavior at the point of use
- capture local variables from the surrounding scope
- keep code shorter and easier to read
- avoid creating small throwaway classes or helper functions
What problem they solve
The biggest problem lambdas solve is passing custom logic that depends on nearby local state.
For example, suppose you want to count numbers greater than a local variable limit:
int limit = 10;
Before lambdas, it was awkward to pass limit into a comparison function for an algorithm. With a lambda, you can capture it directly:
int count = std::count_if(numbers.begin(), numbers.end(), [limit](int n) {
return n > limit;
});
That is concise, local, and easy to understand.
Why this matters in real programming
In real code, developers often need short custom actions for:
- sorting data
- filtering collections
- event handlers
- callbacks
- thread tasks
- deferred work
- custom predicates and transformations
Lambdas make these tasks much cleaner because the logic stays close to the place where it is used.
Important idea: lambdas are function objects
A lambda is not exactly a plain function. It creates a unique unnamed type that behaves like an object with operator().
This matters because:
- lambdas can store captured values as object state
- each lambda has its own type
- they are often more flexible than raw function pointers
So, a lambda is best understood as a compact way to define a callable object inline.
Mental Model
Think of a lambda as a temporary worker you hire for one small job.
- A regular function is like a permanent employee with a name and a fixed office.
- A functor class is like hiring someone but first building a full personnel file.
- A lambda is like saying: "For this one task, right here, do this exact thing."
The capture list is the information you hand to that worker before they start.
[x]means: give the worker a copy ofx[&x]means: let the worker use the originalx[=]means: copy everything needed[&]means: use everything needed by reference
This model helps explain why lambdas are so useful in algorithms: you can send a tiny piece of behavior together with the local data it needs.
Syntax and Examples
General syntax
[capture](parameters) -> return_type {
// body
}
In many cases, C++ can infer the return type, so -> return_type is optional.
Small example
#include <iostream>
int main() {
auto greet = []() {
std::cout << "Hello from a lambda\n";
};
greet();
}
What this does
[]is the capture list()is the parameter list- the body works like a function body
greetstores the lambda objectgreet()calls it
Example with parameters
#include <iostream>
int main() {
auto multiply = [](int a, int b) {
return a * b;
};
std::cout << multiply(3, 4) << '\n';
}
This prints:
12
Example with capture
#include <iostream>
int main() {
int bonus = 5;
auto addBonus = [bonus](int score) {
return score + bonus;
};
std::cout << addBonus(10) << '\n';
}
Why capture is important
The lambda uses bonus, which exists outside the lambda. Without capture, the lambda cannot access it.
Example with std::sort
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {4, 1, 7, 3, 9};
std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
return a > b;
});
for (int n : numbers) {
std::cout << n << ' ';
}
}
This sorts the numbers in descending order.
Why a lambda fits here
The comparison logic is short and only needed once. Creating a separate named function would add unnecessary distance and boilerplate.
Common capture forms
[x] // capture x by value
[&x] // capture x by reference
[=] // capture used outer variables by value
[&] // capture used outer variables by reference
[&, x] // default by reference, but x by value
[=, &x] // default by value, but x by reference
Mutable lambda
By default, captured-by-value variables are treated as read-only inside the lambda. Use mutable if you want to modify the lambda's internal copy.
#include <iostream>
int main() {
int x = 10;
auto f = [x]() mutable {
x += 5;
std::cout << x << '\n';
};
f();
std::cout << x << '\n';
}
Output:
15
10
The original x remains unchanged because the lambda modified its own copy.
Step by Step Execution
Consider this example:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {3, 8, 12, 5, 20};
int limit = 10;
int count = std::count_if(numbers.begin(), numbers.end(), [limit](int n) {
return n > limit;
});
std::cout << count << '\n';
}
Step-by-step
1. A vector is created
std::vector<int> numbers = {3, 8, 12, 5, 20};
The vector contains 5 values.
2. A local variable is created
int limit = 10;
This value will be used inside the lambda.
3. std::count_if starts scanning the vector
std::count_if(numbers.begin(), numbers.end(), ...)
It checks each element and counts how many satisfy a condition.
4. The lambda is passed as the condition
[limit](int n) {
return n > limit;
}
This means:
- capture
limitby value - take one parameter
n - return
trueifnis greater thanlimit
5. The algorithm calls the lambda for each element
It behaves roughly like this:
n = 3→3 > 10isfalsen = 8→8 > 10isfalsen = 12→12 > 10istruen = 5→5 > 10isfalsen = 20→20 > 10istrue
6. The count is stored
There are 2 values greater than 10, so:
int count = 2;
7. The result is printed
std::cout << count << '\n';
Output:
2
Key takeaway
The lambda gives std::count_if a custom rule without needing a separate named function. It also uses the local variable limit, which is one of the main reasons lambdas are so useful.
Real World Use Cases
1. Custom sorting
You often need to sort objects by a specific field.
std::sort(users.begin(), users.end(), [](const User& a, const User& b) {
return a.age < b.age;
});
Useful in:
- user lists
- product catalogs
- leaderboard ranking
2. Filtering data
Lambdas are commonly used with algorithms like std::find_if, std::count_if, and std::remove_if.
auto it = std::find_if(tasks.begin(), tasks.end(), [](const Task& t) {
return t.completed;
});
Useful in:
- task apps
- log analysis
- data cleanup scripts
3. Callbacks and event handling
A lambda can define a small action to run when something happens.
button.onClick([]() {
std::cout << "Button clicked\n";
});
Useful in:
- GUI applications
- game engines
- event-driven systems
4. Thread work
Lambdas are often used to define a task for a thread.
#include <thread>
int value = 42;
std::thread worker([value]() {
std::cout << value << '\n';
});
worker.join();
Useful in:
- background processing
- async jobs
- server tasks
5. Local helper logic
Sometimes a piece of logic is only needed in one function. A lambda avoids polluting the wider namespace.
auto isValid = [](int x) {
return x >= 0 && x <= 100;
};
Useful in:
- validation
- parsing
- one-off transformations
Real Codebase Usage
In real projects, lambdas are rarely used just because they are shorter. They are used because they help keep behavior close to where it is needed.
Common patterns
Guard-like checks inside algorithms
auto it = std::find_if(items.begin(), items.end(), [](const Item& item) {
return !item.name.empty();
});
This keeps simple conditions inline and readable.
Validation rules
auto hasInvalidUser = std::any_of(users.begin(), users.end(), [](const User& user) {
return user.id <= 0 || user.email.empty();
});
This pattern appears in data validation and API input checks.
Transforming collections
std::vector<int> lengths;
std::transform(words.begin(), words.end(), std::back_inserter(lengths), [](const std::string& word) {
return static_cast<int>(word.size());
});
This is common in data processing code.
Error handling and deferred actions
Lambdas are often passed into APIs that retry work, schedule tasks, or handle failures.
auto operation = []() {
// do some work
};
Capturing configuration or context
int threshold = 100;
auto isLarge = [threshold](int value) {
return value > threshold;
};
This is helpful when business rules depend on local settings.
Good real-code habits
- Use lambdas for short, local behavior.
- Prefer them when the logic is only used once.
- Avoid very long lambdas; extract them into a named function if they become hard to read.
- Be careful when capturing by reference, especially if the lambda outlives the current scope.
Common Mistakes
1. Forgetting to capture a variable
Broken code:
int limit = 10;
auto f = [](int x) {
return x > limit;
};
This fails because limit is not available inside the lambda.
Correct version:
int limit = 10;
auto f = [limit](int x) {
return x > limit;
};
2. Capturing by reference when the lambda lives longer than the variable
Broken idea:
auto makeLambda() {
int x = 5;
return [&x]() {
return x;
};
}
This is dangerous because x no longer exists after the function returns.
Safer version:
auto makeLambda() {
int x = 5;
return [x]() {
return x;
};
}
3. Using [&] or [=] too casually
These defaults are convenient, but they can hide what is actually being captured.
Less explicit:
auto f = [&](int value) {
return value + offset;
};
Clearer when practical:
auto f = [&offset](int value) {
return value + offset;
};
4. Expecting value capture to modify the original variable
Broken expectation:
int total = 0;
auto f = [total]() mutable {
total += 10;
};
f();
// total is still 0
The lambda changed its copy, not the original variable.
To change the original:
int total = 0;
auto f = [&total]() {
total += 10;
};
5. Making lambdas too large
If a lambda grows to many lines, it becomes harder to read.
A good rule:
- short local logic -> lambda
- reusable or complex logic -> named function or class
6. Assuming all lambdas are function pointers
Captureless lambdas can often convert to function pointers, but capturing lambdas cannot.
auto a = []() { return 1; }; // may convert
int x = 5;
auto b = [x]() { return x; }; // not a plain function pointer
Comparisons
Lambda vs other ways to provide behavior
| Approach | Best for | Can use local state easily? | Boilerplate | Reusability |
|---|---|---|---|---|
| Regular function | Reusable logic | No | Low | High |
| Function pointer | Simple callback APIs | No | Low | Medium |
| Functor class | Stateful reusable callable objects | Yes | High | High |
| Lambda | Short local behavior | Yes | Low | Medium |
Lambda vs named function
| Feature | Lambda | Named function |
|---|---|---|
| Defined inline | Yes | No |
| Can capture local variables | Yes | No |
| Good for one-off logic | Yes | Sometimes |
| Good for large reusable logic | Usually no | Yes |
Capture by value vs capture by reference
| Capture style | Syntax | What it means | When to use |
|---|---|---|---|
| By value | [x] | Makes a copy of x | When you want safety or a snapshot |
| By reference | [&x] | Uses the original x | When changes should affect the original |
When not to use a lambda
Avoid a lambda when:
- the logic is used in many places
- the code is long or complex
- the behavior needs a descriptive name
- lifetime and reference capture would be risky
Cheat Sheet
Quick syntax
[]() { }
[](int x) { return x * 2; }
[x]() { return x; }
[&x]() { x++; }
[=]() { /* capture used variables by value */ }
[&]() { /* capture used variables by reference */ }
[x]() mutable { x++; }
Parts of a lambda
[capture](parameters) -> return_type {
// body
}
capture: outer variables to bring inparameters: inputs to the lambdareturn_type: optional if C++ can infer itbody: code to run
Common uses
std::sortstd::find_ifstd::count_ifstd::for_eachstd::transform- callbacks
- threads
Capture rules
[x]copiesx[&x]referencesx[=]copies used outer variables[&]references used outer variablesmutablelets you modify captured-by-value copies
Good habits
- Use lambdas for short local logic
- Prefer explicit captures when clarity matters
- Avoid returning lambdas that capture dead references
- Extract complex lambdas into named functions
Edge case to remember
A lambda with captures is not the same as a plain function pointer.
FAQ
What is a lambda expression in C++?
A lambda expression is an anonymous callable object that lets you write function-like logic inline, often right where it is passed to an algorithm or callback.
Why use lambdas instead of regular functions in C++?
Use lambdas when the logic is short, local, and needs access to nearby variables. Regular functions are better for reusable logic that deserves a name.
What does the capture list mean in a C++ lambda?
The capture list controls which outside variables the lambda can use, and whether it gets them by value or by reference.
When should I capture by value vs by reference?
Capture by value when you want a safe copy or snapshot. Capture by reference when the lambda must modify the original variable or avoid copying, but be careful with lifetimes.
Are lambdas faster than normal functions in C++?
Not necessarily, but they are often efficient and can be inlined by the compiler. The main benefit is usually cleaner and more expressive code.
Can a C++ lambda return a value?
Yes. A lambda can return values just like a normal function, and the return type is often inferred automatically.
Can I store a lambda in a variable?
Yes. You can usually store it with auto, or in std::function when you need a common callable wrapper.
When should I avoid using lambdas?
Avoid them when the logic is long, reused in many places, or needs a clear descriptive name.
Mini Project
Description
Build a small C++ program that manages a list of numbers and uses lambda expressions to process them. This project demonstrates the most common beginner use cases for lambdas: filtering, counting, and sorting with local rules.
Goal
Create a program that counts numbers above a threshold, removes unwanted values, and sorts the remaining values using lambda expressions.
Requirements
- Create a
std::vector<int>with at least 8 numbers. - Use a lambda with
std::count_ifto count how many values are greater than a chosen threshold. - Use a lambda with
std::remove_ifto remove values below a minimum allowed value. - Use a lambda with
std::sortto sort the remaining values in descending order. - Print the results after each step.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.