A lambda expression in C++ is an anonymous, inline function that can be defined exactly where it is needed — directly inside another expression or statement. Introduced in C++11, lambdas provide a concise way to create small function objects without the boilerplate of writing a named function or functor class. They can capture variables from the surrounding scope, accept parameters, specify return types, and be stored in variables or passed directly to algorithms.
Introduction
Before C++11, whenever you needed to pass behavior to an algorithm — for example, a custom comparator to std::sort or a predicate to std::find_if — you had two options: write a standalone named function, or write a functor class (a class with operator() overloaded). Both approaches work, but both require you to jump away from the logic you are writing, name something, and define it elsewhere. For small, one-off operations, this friction breaks the flow of reading and writing code.
C++11 introduced lambda expressions to solve this problem elegantly. A lambda lets you write an anonymous function inline, right where you need it. It can capture variables from the surrounding scope, be assigned to a variable, passed to a function, stored in a container, and even called immediately. Lambdas have become one of the most widely used features in modern C++ — they appear in virtually every contemporary codebase.
This article teaches you everything you need to know about lambda expressions in C++. You will start with the basic syntax and work progressively through capture modes, mutable lambdas, return type deduction, generic lambdas (C++14), immediately-invoked lambdas, lambdas with std::function, and advanced patterns. Every concept is illustrated with practical, runnable examples and thorough explanations.
Lambda Syntax: Anatomy of a Lambda
Every lambda expression has the following structure:
[capture list](parameter list) specifiers -> return type { body }Let’s break down each part:
[capture list]— Specifies which variables from the surrounding scope the lambda can access, and how (by value or by reference). This is the part that makes lambdas special compared to regular functions.(parameter list)— The parameters the lambda accepts when called. Optional if there are no parameters.- specifiers — Optional keywords like
mutable,constexpr,noexcept. -> return type— Optional explicit return type. If omitted, the compiler deduces it.{ body }— The function body.
Here is the simplest possible lambda:
#include <iostream>
using namespace std;
int main() {
// A lambda that takes no parameters and returns nothing
auto greet = []() {
cout << "Hello from a lambda!" << endl;
};
greet(); // Call it like a regular function
// A lambda with parameters
auto add = [](int a, int b) {
return a + b;
};
cout << "3 + 4 = " << add(3, 4) << endl;
// A lambda called immediately (no need to store it)
int result = [](int x) { return x * x; }(7);
cout << "7 squared = " << result << endl;
return 0;
}Output:
Hello from a lambda!
3 + 4 = 7
7 squared = 49Step-by-step explanation:
auto greet = []() { ... }defines a lambda and stores it ingreet. Theautotype is necessary because a lambda’s type is a unique, unnamed type generated by the compiler — you cannot write its type explicitly.greet()calls it like any normal function.auto add = [](int a, int b) { return a + b; }defines a lambda that accepts two integers and returns their sum. The return type is deduced automatically asintfrom thereturnstatement.[](int x) { return x * x; }(7)is an immediately-invoked lambda expression (IILE). The lambda is defined and called in one expression. The(7)at the end passes7as the argument and the expression evaluates to49.
Before Lambdas vs. After: The Motivation
To appreciate lambdas, it helps to see what code looked like before them. Consider sorting a vector of strings by length:
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
// Pre-C++11: standalone function
bool shorterFirst(const string& a, const string& b) {
return a.size() < b.size();
}
// Pre-C++11: functor class
struct ShorterFirst {
bool operator()(const string& a, const string& b) const {
return a.size() < b.size();
}
};
int main() {
vector<string> words = {"banana", "fig", "apple", "date", "kiwi"};
// Method 1: standalone function
sort(words.begin(), words.end(), shorterFirst);
for (const auto& w : words) cout << w << " ";
cout << endl;
// Method 2: functor
sort(words.begin(), words.end(), ShorterFirst{});
for (const auto& w : words) cout << w << " ";
cout << endl;
// Method 3: lambda (C++11 and later) — inline, no extra definitions needed
sort(words.begin(), words.end(),
[](const string& a, const string& b) {
return a.size() < b.size();
});
for (const auto& w : words) cout << w << " ";
cout << endl;
return 0;
}Output:
fig fig fig fig fig
fig fig fig fig fig
fig kiwi date apple bananaStep-by-step explanation:
- The standalone function
shorterFirstrequires a separate definition outsidemain(). For simple comparators like this, this scatters related logic across different parts of the file. - The functor
ShorterFirstrequires an entire class definition for what is essentially a one-liner. The class has no reuse value outside this single call site. - The lambda version defines the comparison logic exactly where it is used. There is no extra function or class. The code reads naturally: “sort these words, using the rule that shorter strings come first.”
- All three methods produce identical results. The lambda version is the most readable and the most maintainable for this kind of small, local operation.
The Capture List: Accessing Surrounding Variables
The most distinctive feature of lambdas — the part that makes them more than just anonymous functions — is the capture list. A lambda can capture variables from its enclosing scope and use them inside its body.
Capture by Value
Capturing by value makes a copy of the variable at the time the lambda is created. Changes to the original after lambda creation do not affect the lambda’s copy.
#include <iostream>
using namespace std;
int main() {
int threshold = 10;
// Capture threshold by value
auto isAbove = [threshold](int x) {
return x > threshold;
};
cout << "5 above threshold? " << isAbove(5) << endl; // 0 (false)
cout << "15 above threshold? " << isAbove(15) << endl; // 1 (true)
// Changing threshold after lambda creation has NO effect on the lambda
threshold = 20;
cout << "15 above threshold (after change)? " << isAbove(15) << endl; // Still 1
cout << "threshold in main: " << threshold << endl; // 20
return 0;
}Output:
5 above threshold? 0
15 above threshold? 1
15 above threshold (after change)? 1
threshold in main: 20Step-by-step explanation:
[threshold]capturesthresholdby value. At the moment the lambda is created, a copy ofthreshold(value10) is stored inside the lambda object.- After
threshold = 20, the original variable changes, but the lambda’s internal copy remains10. This is whyisAbove(15)still returnstrue(15 > 10) even after the change. - Capture by value is useful when you want a snapshot of a variable’s current value that the lambda will use consistently regardless of what happens to the original.
Capture by Reference
Capturing by reference gives the lambda a reference to the original variable. Changes through the lambda affect the original, and changes to the original after capture are visible inside the lambda.
#include <iostream>
using namespace std;
int main() {
int counter = 0;
// Capture counter by reference
auto increment = [&counter]() {
counter++; // Directly modifies the original variable
};
increment();
increment();
increment();
cout << "counter = " << counter << endl; // 3
// Lambda sees changes to the original
counter = 100;
auto showCounter = [&counter]() {
cout << "lambda sees counter = " << counter << endl;
};
showCounter(); // Prints 100
counter = 200;
showCounter(); // Prints 200 — lambda tracks the original
return 0;
}Output:
counter = 3
lambda sees counter = 100
lambda sees counter = 200Step-by-step explanation:
[&counter]capturescounterby reference. The lambda has a direct reference into the original variable’s memory location.- Each call to
increment()modifiescounterdirectly. After three calls,counteris3in the outer scope. showCountercapturescounterby reference. Whencounterchanges to100and then200, the lambda sees the updated value each time it is called.- Warning: Never capture a local variable by reference in a lambda that outlives the variable. If the lambda is stored and called after the captured variable goes out of scope, the reference becomes dangling — a common source of undefined behavior.
Capture All by Value: [=]
[=] captures all local variables used inside the lambda body by value.
#include <iostream>
using namespace std;
int main() {
int base = 100;
int multiplier = 3;
// Captures both base and multiplier by value
auto compute = [=](int x) {
return base + x * multiplier;
};
cout << compute(5) << endl; // 100 + 5*3 = 115
cout << compute(10) << endl; // 100 + 10*3 = 130
base = 999; // Does not affect lambda
cout << compute(5) << endl; // Still 115
return 0;
}Capture All by Reference: [&]
[&] captures all local variables used inside the lambda body by reference.
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> results;
int sum = 0;
int count = 0;
// Captures results, sum, and count all by reference
auto record = [&](int value) {
results.push_back(value);
sum += value;
count++;
};
record(10);
record(20);
record(30);
cout << "Count: " << count << endl; // 3
cout << "Sum: " << sum << endl; // 60
cout << "Results: ";
for (int r : results) cout << r << " ";
cout << endl;
return 0;
}Output:
Count: 3
Sum: 60
Results: 10 20 30Mixed Captures
You can mix capture modes — capturing some variables by value and others by reference:
#include <iostream>
#include <string>
using namespace std;
int main() {
string prefix = "Item: ";
int count = 0;
// prefix captured by value (snapshot), count by reference (shared)
auto label = [prefix, &count](const string& name) -> string {
count++;
return prefix + name + " #" + to_string(count);
};
cout << label("Apple") << endl; // Item: Apple #1
cout << label("Banana") << endl; // Item: Banana #2
prefix = "CHANGED"; // Does not affect lambda
cout << label("Cherry") << endl; // Item: Cherry #3 (prefix unchanged)
cout << "Total items labeled: " << count << endl; // 3
return 0;
}Output:
Item: Apple #1
Item: Banana #2
Item: Cherry #3
Total items labeled: 3Step-by-step explanation:
[prefix, &count]capturesprefixby value (a copy of"Item: "is stored in the lambda) andcountby reference (the lambda has a reference to the originalcountvariable).- Even after
prefix = "CHANGED", the lambda’s internal copy ofprefixremains"Item: ". The lambda uses its captured snapshot. countis shared between the lambda and the outer scope. Each call increments the originalcount, and the change is visible both inside the lambda (for the label number) and outside (the finalcout).- The explicit
-> stringreturn type annotation is optional here (the compiler could deduce it), but it makes the intent clear and is good practice for non-trivial lambdas.
Mutable Lambdas: Modifying Captured-by-Value Variables
By default, a lambda that captures by value cannot modify its captured copies — the lambda’s operator() is implicitly const. If you want to modify a captured-by-value variable inside the lambda, you must add the mutable specifier.
#include <iostream>
using namespace std;
int main() {
int callCount = 0;
// Without mutable: captured-by-value variables are read-only
// auto counter = [callCount]() { callCount++; }; // COMPILE ERROR
// With mutable: can modify the copy
auto counter = [callCount]() mutable {
callCount++; // Modifies the lambda's COPY — not the original
cout << "Lambda's internal count: " << callCount << endl;
};
counter(); // 1
counter(); // 2
counter(); // 3
// Original is UNCHANGED — the lambda modified its own copy
cout << "Original callCount: " << callCount << endl; // 0
return 0;
}Output:
Lambda's internal count: 1
Lambda's internal count: 2
Lambda's internal count: 3
Original callCount: 0Step-by-step explanation:
- Without
mutable, the lambda’s copy ofcallCountis read-only. Attempting to modify it is a compile error: “cannot assign to a variable captured by copy in a non-mutable lambda.” - With
mutable, the lambda’soperator()is no longerconst, so it can modify the captured copies. But these are the lambda’s own copies — changes do not propagate back to the original variable in the outer scope. - Each call to
counter()increments the lambda’s internalcallCount. After three calls, the lambda’s copy holds3. But the originalcallCountinmainis still0. - A mutable captured-by-value variable essentially gives the lambda its own state that persists across calls — the lambda object itself is stateful. This is a powerful pattern for implementing stateful callbacks or generators.
Return Type Deduction and Explicit Return Types
The compiler deduces the return type of a lambda from the return statement. If there are no return statements, the return type is void. If there are multiple return statements, they must all return the same type (or types that are implicitly convertible).
For lambdas with complex or ambiguous return types, you can specify the return type explicitly with -> ReturnType.
#include <iostream>
#include <string>
using namespace std;
int main() {
// Return type deduced as int
auto square = [](int x) { return x * x; };
cout << square(5) << endl; // 25
// Return type deduced as string
auto greet = [](const string& name) {
return "Hello, " + name + "!";
};
cout << greet("World") << endl;
// Explicit return type when deduction would be ambiguous
auto safeDivide = [](double a, double b) -> double {
if (b == 0.0) return 0.0; // double
return a / b; // double — consistent
};
cout << safeDivide(10.0, 3.0) << endl; // 3.33333
cout << safeDivide(5.0, 0.0) << endl; // 0
// Explicit type needed when returning different numeric types
auto classify = [](int x) -> string {
if (x > 0) return "positive";
if (x < 0) return "negative";
return "zero";
};
cout << classify(42) << endl;
cout << classify(-7) << endl;
cout << classify(0) << endl;
return 0;
}Output:
25
Hello, World!
3.33333
0
positive
negative
zeroStep-by-step explanation:
- For
squareandgreet, the return types (intandstring) are unambiguous and deduced automatically. safeDividereturnsdoublein both branches. Even though the deduction would work here, the explicit-> doublemakes the intent immediately clear to readers.classifyreturnsstringliterals in all branches. The explicit-> stringis not strictly necessary (allreturnexpressions areconst char*which convert tostring), but it makes the return type of the lambda obvious at a glance and avoids subtle type conversion questions.
Lambdas with STL Algorithms
Lambdas shine brightest when combined with the Standard Template Library’s algorithms. They make algorithm-based code readable and concise.
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <string>
using namespace std;
int main() {
vector<int> numbers = {15, 3, 42, 8, 27, 5, 19, 1, 33, 11};
// --- std::sort with custom comparator ---
sort(numbers.begin(), numbers.end(),
[](int a, int b) { return a > b; }); // Descending order
cout << "Sorted descending: ";
for (int n : numbers) cout << n << " ";
cout << endl;
// --- std::find_if: find first number > 20 ---
auto it = find_if(numbers.begin(), numbers.end(),
[](int n) { return n > 20; });
if (it != numbers.end())
cout << "First > 20: " << *it << endl;
// --- std::count_if: count even numbers ---
int evenCount = count_if(numbers.begin(), numbers.end(),
[](int n) { return n % 2 == 0; });
cout << "Even numbers: " << evenCount << endl;
// --- std::for_each: print with formatting ---
cout << "All numbers: ";
for_each(numbers.begin(), numbers.end(),
[](int n) { cout << "[" << n << "] "; });
cout << endl;
// --- std::transform: square every element ---
vector<int> squares(numbers.size());
transform(numbers.begin(), numbers.end(), squares.begin(),
[](int n) { return n * n; });
cout << "Squared: ";
for (int s : squares) cout << s << " ";
cout << endl;
// --- std::accumulate with lambda: sum of squares ---
int sumOfSquares = accumulate(numbers.begin(), numbers.end(), 0,
[](int acc, int n) { return acc + n * n; });
cout << "Sum of squares: " << sumOfSquares << endl;
// --- std::remove_if: remove numbers less than 10 ---
numbers.erase(
remove_if(numbers.begin(), numbers.end(),
[](int n) { return n < 10; }),
numbers.end());
cout << "After removing < 10: ";
for (int n : numbers) cout << n << " ";
cout << endl;
return 0;
}Output:
Sorted descending: 42 33 27 19 15 11 8 5 3 1
First > 20: 42
Even numbers: 2
All numbers: [42] [33] [27] [19] [15] [11] [8] [5] [3] [1]
Squared: 1764 1089 729 361 225 121 64 25 9 1
Sum of squares: 4388
After removing < 10: 42 33 27 19 15 11Step-by-step explanation:
sortwith[](int a, int b) { return a > b; }sorts in descending order. The lambda acts as the comparator, returningtruewhenashould come beforeb.find_ifwith[](int n) { return n > 20; }locates the first element satisfying the predicate. The resultitis an iterator to that element.count_ifcounts elements where the predicate is true. The lambda[](int n) { return n % 2 == 0; }is the predicate.transformapplies the lambda to each element and stores results insquares. Each element is replaced by its square.accumulatewith a binary lambda builds a running total. The binary accumulator lambda takes the current accumulated value and the next element, returning the updated accumulation.- The erase-remove idiom with
remove_ifremoves elements satisfying the predicate.remove_ifmoves matching elements to the end and returns an iterator to where the removed region starts;erasethen physically removes them.
Capturing this in Member Functions
When a lambda is defined inside a member function of a class, you can capture this to access the class’s member variables and methods.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class DataProcessor {
private:
vector<int> data_;
int minValue_;
int maxValue_;
public:
DataProcessor(vector<int> data, int minVal, int maxVal)
: data_(move(data)), minValue_(minVal), maxValue_(maxVal) {}
// Lambda captures 'this' to access member variables
vector<int> filter() const {
vector<int> result;
for_each(data_.begin(), data_.end(),
[this, &result](int val) { // Capture this
if (val >= minValue_ && val <= maxValue_) {
result.push_back(val);
}
});
return result;
}
// Lambda captures 'this' and modifies member data
void normalize() {
if (data_.empty()) return;
int maxVal = *max_element(data_.begin(), data_.end());
// [this] captures access to data_ member
transform(data_.begin(), data_.end(), data_.begin(),
[this, maxVal](int val) -> int {
return (maxVal > 0) ? (val * 100 / maxVal) : 0;
});
}
void print() const {
cout << "Data: ";
for (int v : data_) cout << v << " ";
cout << endl;
}
};
int main() {
DataProcessor proc({5, 12, 3, 18, 7, 25, 9, 14}, 5, 15);
cout << "Original: ";
proc.print();
auto filtered = proc.filter();
cout << "Filtered (5-15): ";
for (int v : filtered) cout << v << " ";
cout << endl;
proc.normalize();
cout << "Normalized: ";
proc.print();
return 0;
}Output:
Original: Data: 5 12 3 18 7 25 9 14
Filtered (5-15): 5 12 7 9 14
Normalized: Data: 20 48 12 72 28 100 36 56Step-by-step explanation:
[this, &result]captures the current object pointer (this) andresultby reference. Inside the lambda,minValue_andmaxValue_are accessed asthis->minValue_andthis->maxValue_— but you can omitthis->since the capture makes all member variables available.[this, maxVal]innormalizecapturesthis(for access todata_) andmaxValby value (a snapshot of the maximum value at the time of capture).- In C++17, you can use
[*this]to capture the entire object by value (making a copy), rather than by pointer. This is useful for asynchronous code where the object might be destroyed before the lambda runs. - Capturing
thisin asynchronous or long-lived lambdas requires care: if theDataProcessorobject is destroyed before the lambda runs,thisbecomes a dangling pointer.
Generic Lambdas (C++14): Auto Parameters
C++14 introduced generic lambdas, where parameters can be declared auto, making the lambda behave like a template function. The compiler generates a separate instantiation for each argument type.
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
int main() {
// Generic lambda: works with any printable type
auto print = [](auto value) {
cout << value << endl;
};
print(42);
print(3.14);
print("hello");
print(string("world"));
// Generic comparator: works with any type supporting <
auto lessThan = [](auto a, auto b) {
return a < b;
};
cout << "3 < 5: " << lessThan(3, 5) << endl;
cout << "'apple' < 'banana': " << lessThan(string("apple"), string("banana")) << endl;
// Generic lambda for a max finder
auto findMax = [](const auto& container) -> decltype(auto) {
return *max_element(container.begin(), container.end());
};
vector<int> ints = {3, 1, 4, 1, 5, 9, 2, 6};
vector<string> words = {"banana", "apple", "cherry", "date"};
cout << "Max int: " << findMax(ints) << endl;
cout << "Max word (alphabetically): " << findMax(words) << endl;
// Generic transform
auto doubleValue = [](auto x) { return x * 2; };
vector<int> doubled(ints.size());
transform(ints.begin(), ints.end(), doubled.begin(), doubleValue);
cout << "Doubled ints: ";
for (int v : doubled) cout << v << " ";
cout << endl;
vector<double> dvals = {1.5, 2.5, 3.5};
vector<double> dDoubled(dvals.size());
transform(dvals.begin(), dvals.end(), dDoubled.begin(), doubleValue);
cout << "Doubled doubles: ";
for (double v : dDoubled) cout << v << " ";
cout << endl;
return 0;
}Output:
42
3.14
hello
world
3 < 5: 1
'apple' < 'banana': 1
Max int: 9
Max word (alphabetically): date
Doubled ints: 6 2 8 2 10 18 4 12
Doubled doubles: 3 5 7Step-by-step explanation:
auto print = [](auto value) { ... }is a generic lambda. When called with42(int), the compiler instantiates a version withint. When called with"hello"(const char*), it instantiates another. One lambda definition, multiple generated specializations.findMaxusesconst auto&to avoid copying the container anddecltype(auto)to preserve the exact return type (a reference to the maximum element). This works forvector<int>andvector<string>with the same lambda.doubleValueworks for bothvector<int>andvector<double>. Thex * 2expression is instantiated asint * 2anddouble * 2respectively.- Generic lambdas are essentially syntactic sugar for a template
operator(). They are the predecessor to C++20’s abbreviated function templates and are extremely powerful for writing flexible, reusable inline operations.
Storing Lambdas: std::function
Lambda expressions have unique, unnamed types that the compiler generates. While you can use auto to store a lambda locally, if you need to store a lambda in a struct member, pass it through non-template APIs, or store different lambdas in a collection, you use std::function<ReturnType(ArgTypes...)>.
#include <iostream>
#include <functional>
#include <vector>
#include <string>
using namespace std;
// Function that takes a callback
void processItems(const vector<int>& items,
function<void(int)> callback) {
for (int item : items) {
callback(item);
}
}
// Event handler system using std::function
class Button {
public:
string label;
function<void()> onClick; // Stores any callable with no args, no return
Button(string lbl) : label(lbl) {}
void click() {
cout << "Button '" << label << "' clicked" << endl;
if (onClick) onClick(); // Check if handler is set before calling
}
};
int main() {
vector<int> data = {1, 2, 3, 4, 5};
// Pass lambda as std::function parameter
processItems(data, [](int x) {
cout << x * x << " ";
});
cout << endl;
// Store different lambdas in same std::function variable
function<int(int, int)> op;
op = [](int a, int b) { return a + b; };
cout << "Add: " << op(3, 4) << endl;
op = [](int a, int b) { return a * b; };
cout << "Multiply: " << op(3, 4) << endl;
// std::function in a struct member — event system
Button btn("Submit");
int clickCount = 0;
btn.onClick = [&clickCount, &btn]() {
clickCount++;
cout << "Handling click #" << clickCount
<< " for '" << btn.label << "'" << endl;
};
btn.click();
btn.click();
btn.click();
// Collection of different operations
vector<function<double(double)>> transformers = {
[](double x) { return x * 2; },
[](double x) { return x * x; },
[](double x) { return x + 100; },
};
double value = 5.0;
for (auto& transform : transformers) {
cout << "Transformed " << value << " -> " << transform(value) << endl;
}
return 0;
}Output:
1 4 9 16 25
Add: 7
Multiply: 12
Button 'Submit' clicked
Handling click #1 for 'Submit'
Button 'Submit' clicked
Handling click #2 for 'Submit'
Button 'Submit' clicked
Handling click #3 for 'Submit'
Transformed 5 -> 10
Transformed 5 -> 25
Transformed 5 -> 105Step-by-step explanation:
function<void(int)>is a type-erased wrapper that can store any callable that takesintand returnsvoid. This includes lambdas, regular functions, functors, andstd::bindexpressions.function<int(int, int)> opdemonstrates storing different lambdas in the samefunctionvariable by reassignment. The variable can hold any callable with the matching signature.Button::onClickis afunction<void()>member. Any lambda, free function, or method withvoid()signature can be assigned to it. This is the classic observer/callback pattern.vector<function<double(double)>> transformersstores a heterogeneous collection of callables that all share the same signature. This enables runtime dispatch — choosing which operation to apply from a list.- Performance note:
std::functionuses type erasure and may involve heap allocation for large lambda objects. For performance-critical inner loops, preferautoor template parameters overstd::function.
Lambda Comparison Table: Capture Modes
| Capture Syntax | What It Captures | How | Use Case |
|---|---|---|---|
[] | Nothing | — | No external variables needed |
[x] | Variable x | By value (copy) | Read a snapshot; source may change |
[&x] | Variable x | By reference | Modify original; need up-to-date value |
[=] | All used locals | By value (all) | Convenience snapshot; multiple variables |
[&] | All used locals | By reference (all) | Convenience; modify multiple originals |
[=, &x] | All by value; x by ref | Mixed | Most by snapshot, one needs write-back |
[&, x] | All by ref; x by value | Mixed | Most need write-back, one is read-only |
[this] | Current object pointer | By pointer | Access class members in member functions |
[*this] | Current object | By value (copy) | Safe for async; object may be destroyed |
Advanced Patterns: Recursive Lambdas
A lambda cannot capture itself by name (since it doesn’t have one when the capture list is evaluated), but you can enable recursion using std::function or a Y-combinator pattern.
#include <iostream>
#include <functional>
using namespace std;
int main() {
// Method 1: Use std::function to enable self-reference
function<int(int)> factorial = [&factorial](int n) -> int {
return n <= 1 ? 1 : n * factorial(n - 1);
};
cout << "5! = " << factorial(5) << endl;
cout << "10! = " << factorial(10) << endl;
// Method 2: Pass the lambda to itself (C++14 generic lambda)
auto fibonacci = [](auto self, int n) -> int {
if (n <= 1) return n;
return self(self, n - 1) + self(self, n - 2);
};
cout << "fib(10) = " << fibonacci(fibonacci, 10) << endl;
return 0;
}Output:
5! = 120
10! = 3628800
fib(10) = 55Step-by-step explanation:
- Method 1:
function<int(int)> factorial = [&factorial](int n) -> int { ... }stores the lambda in a namedstd::functionvariable and captures that variable by reference. Inside the lambda,factorial(n-1)is a valid recursive call becausefactorialis a named, in-scopestd::function. - Method 2: The generic lambda takes itself as the first argument (
auto self). Callingfibonacci(fibonacci, 10)passes the lambda to itself, enabling recursion. This avoidsstd::functionoverhead but requires passing the lambda explicitly. - Note that
std::functioncarries overhead from type erasure. For high-performance recursive lambdas, Method 2 or a traditional named function may be preferable.
Immediately-Invoked Lambda Expressions (IILE)
One underappreciated lambda use case is the immediately-invoked lambda for complex initialization logic. C++ requires variables to be initialized in their declaration, but sometimes initialization logic is complex. A lambda called immediately can produce the value:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main() {
// Complex initialization with an IILE
const int config = []() {
int val = 10;
val *= 2;
val += 5;
return val;
}();
cout << "config = " << config << endl; // 25
// Initialize a const vector with computed values
const vector<int> primes = []() {
vector<int> result;
for (int n = 2; n <= 30; n++) {
bool isPrime = true;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) { isPrime = false; break; }
}
if (isPrime) result.push_back(n);
}
return result;
}();
cout << "Primes up to 30: ";
for (int p : primes) cout << p << " ";
cout << endl;
// Conditional initialization without ternary nesting
int x = 42;
const string category = [x]() -> string {
if (x < 0) return "negative";
if (x == 0) return "zero";
if (x < 10) return "small";
if (x < 100) return "medium";
return "large";
}();
cout << x << " is " << category << endl;
return 0;
}Output:
config = 25
Primes up to 30: 2 3 5 7 11 13 17 19 23 29
42 is mediumStep-by-step explanation:
const int config = []() { ... }()defines a lambda and calls it immediately. Theconstmodifier meansconfigcannot be changed later. The lambda provides the multi-step initialization logic cleanly.- The
primesvector isconst, meaning it cannot be modified after initialization. Without the lambda, you would need a mutable intermediate vector and aconstreassignment, or a helper function. The IILE initializes it in one clean expression. - The
categoryclassification uses an IILE to select from multiple conditions without nesting ternary operators. The result is aconst stringinitialized with exactly the right value. - IILEs are particularly valuable for initializing
constorconstexprvariables with complex logic that cannot be expressed as a single expression.
Practical Example: A Flexible Event Pipeline
Let’s build a real-world example combining multiple lambda features into a practical event processing pipeline.
#include <iostream>
#include <vector>
#include <functional>
#include <string>
#include <algorithm>
using namespace std;
struct Event {
string type;
int value;
string source;
};
class EventPipeline {
public:
using Filter = function<bool(const Event&)>;
using Transform = function<Event(Event)>;
using Handler = function<void(const Event&)>;
EventPipeline& addFilter(Filter f) {
filters_.push_back(move(f));
return *this;
}
EventPipeline& addTransform(Transform t) {
transforms_.push_back(move(t));
return *this;
}
EventPipeline& addHandler(Handler h) {
handlers_.push_back(move(h));
return *this;
}
void process(Event event) const {
// Apply all filters
for (const auto& filter : filters_) {
if (!filter(event)) return; // Event rejected
}
// Apply all transforms
for (const auto& transform : transforms_) {
event = transform(event);
}
// Deliver to all handlers
for (const auto& handler : handlers_) {
handler(event);
}
}
void processAll(const vector<Event>& events) const {
for (const auto& e : events) process(e);
}
private:
vector<Filter> filters_;
vector<Transform> transforms_;
vector<Handler> handlers_;
};
int main() {
int processedCount = 0;
int highValueCount = 0;
EventPipeline pipeline;
pipeline
// Filter: only process "click" and "submit" events
.addFilter([](const Event& e) {
return e.type == "click" || e.type == "submit";
})
// Filter: ignore events with value < 0
.addFilter([](const Event& e) {
return e.value >= 0;
})
// Transform: normalize source to uppercase
.addTransform([](Event e) {
for (char& c : e.source) c = toupper(c);
return e;
})
// Transform: amplify value for "submit" events
.addTransform([](Event e) {
if (e.type == "submit") e.value *= 10;
return e;
})
// Handler: log every processed event
.addHandler([&processedCount](const Event& e) {
processedCount++;
cout << "[LOG] " << e.type << " from " << e.source
<< " value=" << e.value << endl;
})
// Handler: count high-value events
.addHandler([&highValueCount](const Event& e) {
if (e.value > 50) highValueCount++;
});
vector<Event> events = {
{"click", 5, "button_a"},
{"hover", 1, "menu"}, // Filtered out (not click/submit)
{"submit", 8, "form_b"},
{"click", -3, "button_c"}, // Filtered out (negative value)
{"submit", 3, "form_a"},
{"click", 15, "button_b"},
};
pipeline.processAll(events);
cout << "\nTotal processed: " << processedCount << endl;
cout << "High-value events: " << highValueCount << endl;
return 0;
}Output:
[LOG] click from BUTTON_A value=5
[LOG] submit from FORM_B value=80
[LOG] submit from FORM_A value=30
[LOG] click from BUTTON_B value=15
Total processed: 4
High-value events: 1Step-by-step explanation:
- The pipeline stores
std::functioncollections for filters, transforms, and handlers. Each stage is added via a fluent interface (addFilter(...).addTransform(...)...), returning*thisfor chaining. - Filters are lambdas that return
bool. Two filters are applied in sequence: type check and value check. If either returnsfalse, the event is dropped. - Transforms are lambdas that take and return an
Event. The source is uppercased in the first transform; submit values are amplified in the second. Events are modified as they flow through. - Handlers are lambdas that receive the final
Event. The log handler capturesprocessedCountby reference and increments it. The count handler captureshighValueCountby reference. - The “hover” event is filtered out by the type filter. The negative-value click is filtered out by the value filter. Four events pass all filters and are processed.
- This pattern demonstrates real-world lambda usage: lambdas stored as
std::function, lambda state via captures, lambda composition through multiple stages, and fluent API design with lambdas as building blocks.
Common Mistakes with Lambdas
Mistake 1: Dangling Reference Capture
function<int()> makeCounter() {
int count = 0;
return [&count]() { return ++count; }; // BAD: count is destroyed on return
}
// SAFE: capture by value
function<int()> makeCounter() {
int count = 0;
return [count]() mutable { return ++count; }; // OK: copy lives in lambda
}Never capture local variables by reference in a lambda that outlives the enclosing scope.
Mistake 2: Capturing this in Async Lambdas
class Widget {
void startAsync() {
// BAD: Widget may be destroyed before async work completes
async_do_work([this]() { use(member_); });
// SAFER: capture a shared_ptr to keep Widget alive
auto self = shared_from_this();
async_do_work([self]() { self->use(self->member_); });
}
};Mistake 3: Accidentally Copying Large Objects
vector<int> bigData(1'000'000, 0);
// BAD: copies the entire vector into the lambda
auto process = [bigData]() { ... };
// GOOD: capture by reference (if lambda doesn't outlive bigData)
auto process = [&bigData]() { ... };
// GOOD: move into lambda (transfers ownership)
auto process = [data = move(bigData)]() { ... };Mistake 4: Overusing std::function for Performance-Critical Paths
std::function adds overhead from type erasure and potential heap allocation. In tight loops, prefer auto with template parameters:
// Slow (std::function overhead in every call)
void process(vector<int>& v, function<int(int)> f) { ... }
// Fast (zero overhead — inlined by compiler)
template<typename Func>
void process(vector<int>& v, Func f) { ... }Conclusion
Lambda expressions are one of the most transformative features of modern C++. They allow you to express small, focused pieces of behavior exactly where they are needed — without the ceremony of separate function definitions or functor classes. The result is code that is more readable, more maintainable, and often more efficient.
The capture list is what elevates lambdas beyond simple anonymous functions. By selectively capturing variables by value or by reference, a lambda becomes a powerful closure that can carry state, observe external variables, and modify its environment — all while maintaining clear ownership semantics.
Used with STL algorithms, lambdas transform verbose loop-based code into concise, expressive pipelines. Used with std::function, they enable flexible callback and event systems. Used as immediately-invoked expressions, they initialize complex const variables cleanly. Used as generic lambdas in C++14, they provide template-level flexibility with minimal syntax.
Mastering lambdas — their syntax, capture modes, mutable behavior, storage options, and best practices — is an essential step in writing fluent, idiomatic modern C++. Once they become part of your natural programming vocabulary, you will wonder how you ever wrote C++ without them.








