Lambdas and Functional Programming in C++
Write concise, expressive code with lambda expressions, captures, generic lambdas, and std::function.
Lambdas are anonymous function objects you can define inline, right where they’re used. They make STL algorithms readable, replace boilerplate functor classes, and enable concise callbacks. Before lambdas, passing a custom comparator to std::sort required writing a named struct with an operator(). With lambdas, you write the logic inline in two lines. Since C++11 introduced them and C++14/17/20 expanded them, they’ve become one of the most-used features of modern C++.
Lambda Syntax
A lambda is syntactic sugar for a compiler-generated function object. The compiler creates an anonymous class with operator() and instantiates it — you get a real object with real type, just without a name you have to invent.
// [capture](parameters) -> return_type { body }
auto add = [](int a, int b) -> int { return a + b; };
int result = add(3, 4); // 7
// Return type is usually deduced — the -> return_type is optional
auto square = [](double x) { return x * x; };
The capture list, parameter list, and return type are all optional. The minimal lambda is []{}.
Capture Modes
The capture list controls which variables from the enclosing scope the lambda can access, and how — by value (a copy made at capture time) or by reference (a live reference to the original). Getting captures right is the most important decision when writing a lambda: capture by value for safety when lifetime is uncertain, capture by reference when you need to mutate the original.
#include <iostream>
#include <string>
int main() {
int x = 10;
std::string name = "Alice";
// Capture by value — lambda gets its own copies at the time of creation
auto by_value = [x, name]() {
std::cout << x << " " << name << "\n";
// x = 20; // compile error — value captures are const by default
};
// Capture by reference — lambda sees and can modify the originals
auto by_ref = [&x, &name]() {
x = 20; // modifies the original x
name = "Bob";
};
// Capture all by value — convenient but captures everything, consider carefully
auto all_value = [=]() { std::cout << x << " " << name << "\n"; };
// Capture all by reference — convenient but dangerous if lambda outlives scope
auto all_ref = [&]() { x += 1; };
// Mixed: all by value except x by reference
auto mixed = [=, &x]() { x = 99; };
by_ref();
std::cout << x << "\n"; // 20
std::cout << name << "\n"; // Bob
}
Dangling reference risk: if a lambda captures by reference and outlives the captured variables (e.g., stored in a callback that fires asynchronously), you get undefined behavior. Capture by value when lifetime is uncertain.
Mutable Lambdas
By default, value captures are const inside the lambda body — you can read them but not modify them. Use mutable when you need to modify the lambda’s own copy of a captured variable. The original is still unaffected; mutable only affects the lambda’s internal state.
int counter = 0;
auto increment = [counter]() mutable {
++counter; // modifies the lambda's own copy
return counter;
};
std::cout << increment() << "\n"; // 1
std::cout << increment() << "\n"; // 2
std::cout << counter << "\n"; // 0 — original unchanged
Lambdas with STL Algorithms
This is where lambdas provide the most value. Before lambdas, every custom sort, filter, or transform required a named comparator struct. With lambdas the logic lives right at the call site, making the code’s intent immediately clear.
#include <algorithm>
#include <vector>
#include <iostream>
#include <numeric>
int main() {
std::vector<int> nums = {5, 2, 8, 1, 9, 3, 7};
// Sort descending — no need for a named comparator struct
std::sort(nums.begin(), nums.end(), [](int a, int b) { return a > b; });
// Filter: keep only evens — logic is right at the call site
std::vector<int> evens;
std::copy_if(nums.begin(), nums.end(), std::back_inserter(evens),
[](int n) { return n % 2 == 0; });
// Transform: square each element in-place
std::transform(nums.begin(), nums.end(), nums.begin(),
[](int n) { return n * n; });
// Accumulate with custom operation — product of all evens
int product = std::accumulate(evens.begin(), evens.end(), 1,
[](int acc, int n) { return acc * n; });
}
Generic Lambdas (C++14)
C++14 allows auto parameters in lambdas, making them work like function templates. A single generic lambda can replace multiple overloads and handles any type that supports the required operations.
// Works with any type that supports operator+ — int, double, string, etc.
auto add = [](auto a, auto b) { return a + b; };
std::cout << add(1, 2) << "\n"; // int: 3
std::cout << add(1.5, 2.3) << "\n"; // double: 3.8
std::cout << add(std::string("hi"), "!") << "\n"; // string: "hi!"
// Generic lambda with a C++20 concept constraint — only for arithmetic types
auto print_if_positive = [](auto val) requires (std::is_arithmetic_v<decltype(val)>) {
if (val > 0) std::cout << val << "\n";
};
Immediately Invoked Lambdas
An immediately invoked lambda is defined and called in the same expression. This is useful for initializing a const variable that requires multi-step logic — without the lambda you’d have to either use a non-const variable or write a separate named function.
// The const variable is initialized by complex logic, all in one expression
const int config_value = []() {
const char* env = std::getenv("APP_LEVEL");
if (!env) return 0;
int val = std::atoi(env);
return (val >= 0 && val <= 10) ? val : 0;
}(); // the () at the end calls the lambda immediately
std::function — Type-Erased Callable
std::function<R(Args...)> stores any callable — lambda, function pointer, functor — with a compatible signature. Its benefit is flexibility: it lets you store lambdas in containers or class members where the template type cannot be deduced. Its cost is type erasure, which involves an extra indirection and may heap-allocate the closure.
#include <functional>
#include <vector>
struct Button {
std::string label;
std::function<void()> on_click; // stores any callable — lambda, function pointer, etc.
void click() { if (on_click) on_click(); }
};
int main() {
std::vector<std::function<int(int)>> transforms;
transforms.push_back([](int x) { return x * 2; });
transforms.push_back([](int x) { return x + 10; });
transforms.push_back([](int x) { return x * x; });
int val = 3;
for (auto& f : transforms) val = f(val);
std::cout << val << "\n"; // ((3*2)+10)^2 = 256
}
The cost of std::function is real: type erasure typically involves a virtual call and may heap-allocate the closure. When you don’t need runtime polymorphism, prefer auto or a template parameter.
// Fast — no type erasure, compiler can inline the lambda
template<typename F>
void apply_fast(const std::vector<int>& v, F func) {
for (int x : v) func(x);
}
// Flexible but slower — type erased, virtual dispatch overhead
void apply_flexible(const std::vector<int>& v, std::function<void(int)> func) {
for (int x : v) func(x);
}
Recursive Lambdas
Lambdas cannot refer to themselves by name directly because the name is not in scope during the lambda’s own definition. The C++23 explicit object parameter is the cleanest solution. For older standards, use std::function with capture by reference.
#include <functional>
// C++14/17: use std::function — the lambda captures itself by reference
std::function<int(int)> fib = [&fib](int n) -> int {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
};
// C++23: explicit object parameter (deducing this) — cleaner, no std::function overhead
auto fib23 = [](this auto& self, int n) -> int {
return n <= 1 ? n : self(n - 1) + self(n - 2);
};
Capturing this
Inside a member function, capture this to access the object’s members. In C++17, [*this] captures the entire object by value — safer for asynchronous callbacks where the object might be destroyed before the callback fires.
class Processor {
int threshold_ = 5;
std::vector<int> data_;
public:
void filter() {
// [this] captures the pointer — the lambda must not outlive the object
auto above = [this](int x) { return x > threshold_; };
// C++17: [*this] captures by value — safe for async callbacks
auto above_safe = [*this](int x) { return x > threshold_; };
data_.erase(
std::remove_if(data_.begin(), data_.end(),
[this](int x) { return x <= threshold_; }),
data_.end()
);
}
};
Stateless vs Stateful Lambdas
A lambda with an empty capture list [] is stateless — it carries no data and can decay to a plain function pointer. This matters for C APIs that take a function pointer and cannot accept a closure.
// C API that takes a function pointer
void register_callback(void (*cb)(int));
register_callback([](int x) { /* no captures */ }); // OK — stateless, decays to pointer
int threshold = 5;
// register_callback([threshold](int x) { }); // compile error — stateful lambda
// cannot decay to function pointer
Lambdas pack a lot of power into a small syntax. The key decisions are: what to capture and how (by value for safety, by reference for mutation), whether you need std::function’s flexibility or auto’s performance, and whether a generic lambda makes the code cleaner than a named template.