What you'll learn
Algorithms become flexible when behavior is an argument. Lambdas create small function objects at the point of use; callable wrappers and invocation utilities let behavior cross API boundaries. The central design question is not syntax—it is state and lifetime.
By the end, you'll be able to:
- Write lambdas with parameters, captures, return types, and constraints
- Choose value, reference, initialized, and move captures safely
- Compare lambdas, function objects, function pointers, and std::function
- Invoke callables generically while protecting captured lifetimes
Lambda expressions
A lambda expression constructs an unnamed function object. Its capture list holds state, parameters describe each call, and the body implements behavior. Return type is normally deduced; add -> Type when branches need an explicit common contract.
| Part | Example | Role |
|---|---|---|
| Capture | [limit] | State stored in the closure |
| Parameters | (int value) | Inputs for each call |
| Specifier | mutable | Allows mutation of value-captured state |
| Return | -> bool | Explicit result type |
| Body | { return value < limit; } | Executed behavior |
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> names{"Grace", "Ada", "Bjarne"};
std::ranges::sort(names, [](const std::string& left, const std::string& right) {
return left.size() < right.size();
});
for (const auto& name : names) std::cout << name << ' ';
}Captures and ownership
Value capture stores a copy made when the lambda is created. Reference capture stores an alias and requires the original object to remain alive. Initialized capture can rename, transform, or move a value into the closure, making it the clearest form for owned state.
#include <iostream>
#include <memory>
int main() {
int threshold{10};
auto above = [threshold](int value) { return value > threshold; };
auto resource{std::make_unique<int>(42)};
auto consume = [owned = std::move(resource)] {
std::cout << *owned << '\n';
};
std::cout << std::boolalpha << above(12) << '\n';
consume();
}Watch out
Generic and mutable lambdas
An auto parameter makes the closure's call operator a template. Constraints can narrow the accepted arguments. By default, a lambda's call operator is const; themutable specifier permits changes to its own value-captured state without changing the original external object.
#include <concepts>
#include <iostream>
int main() {
auto twice = []<std::integral T>(T value) { return value * 2; };
auto next_id = [current = 100]() mutable { return ++current; };
std::cout << twice(21) << '\n';
std::cout << next_id() << ' ' << next_id() << '\n';
}Note
Function objects and pointers
A named function object is a class with operator(), useful when behavior needs a documented reusable type. A function pointer stores an address to a compatible free or static function but carries no captured state. Lambdas with no captures can convert to a matching function pointer.
#include <iostream>
struct Clamp {
int low;
int high;
int operator()(int value) const {
if (value < low) return low;
if (value > high) return high;
return value;
}
};
int square(int value) { return value * value; }
int main() {
Clamp percentage{0, 100};
int (*operation)(int){square};
std::cout << percentage(140) << ' ' << operation(6) << '\n';
}std::function and type erasure
std::function<Signature> stores different copyable callables behind one runtime interface. That flexibility can allocate and adds indirect-call overhead, so templates are preferable in performance-sensitive generic code. Use std::functionwhen heterogeneous callables must share one stable stored type.
#include <functional>
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::function<void(std::string)>> handlers;
handlers.emplace_back([](const std::string& event) {
std::cout << "log: " << event << '\n';
});
handlers.emplace_back([count = 0](const std::string&) mutable {
std::cout << "count: " << ++count << '\n';
});
for (auto& handler : handlers) handler("saved");
}Tip
std::function, test it in a Boolean context; invoking an empty wrapper throws std::bad_function_call.invoke, bind_front, and composition
std::invoke uniformly calls functions, function objects, and member pointers.std::bind_front binds leading arguments into a callable without the placeholder syntax of older bind. Simple lambdas often remain the clearest adapter because their capture and call behavior are visible.
#include <functional>
#include <iostream>
#include <string>
struct Greeter {
void greet(const std::string& name) const {
std::cout << "Hello, " << name << "!\n";
}
};
int main() {
Greeter greeter;
std::invoke(&Greeter::greet, greeter, "Maya");
auto greet_maya{std::bind_front(&Greeter::greet, &greeter, "Maya")};
greet_maya();
}- Prefer a template parameter when the callable type can remain generic
- Prefer a function pointer for stateless C-style callback boundaries
- Prefer std::function when stored copyable callables need one runtime type
- Prefer a lambda as a readable local adapter or stateful operation
Recap & quick check
Key takeaways
- A lambda constructs an unnamed function object with explicit call behavior and optional state.
- Value captures own copies; reference captures borrow and can dangle.
- Generic lambdas template their call operator, while mutable permits changing captured copies.
- std::function offers runtime type erasure with flexibility and a cost.
- std::invoke unifies calling syntax, but lifetime remains the callback designer's responsibility.
Quick check
1. When is a value capture copied?
2. What does mutable permit?
3. Which wrapper stores heterogeneous copyable callables behind one signature?
4. What is the main risk of storing a reference-capturing callback?
Next: Module 18 — Exceptions, optional, variant & expected, where APIs represent failure and alternative results deliberately.