Lambdas, algorithms and ranges
Capture correctly, choose between the algorithm and ranges forms, chain lazy views, and avoid iterator invalidation.
Lambdas and captures
#include <algorithm>
#include <vector>
#include <string>
#include <memory>
int main() {
int threshold = 10;
std::vector<int> v{1, 15, 3, 20, 7};
auto by_value = [threshold](int x) { return x > threshold; }; // copies in
auto by_ref = [&threshold](int x) { return x > threshold; }; // sees later changes
auto generic = [](auto a, auto b) { return a < b; }; // templated operator()
// mutable lets the lambda modify its own copies (the call is const otherwise)
auto counter = [n = 0]() mutable { return ++n; };
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
auto it = std::find_if(v.begin(), v.end(), by_value);
// init capture moving an owner into the closure
auto owner = std::make_unique<std::string>("data");
auto task = [p = std::move(owner)]() -> std::size_t { return p->size(); };
// shared state across copies of the closure
auto shared = std::make_shared<int>(0);
auto shared_task = [shared]() { ++*shared; };
}| Capture | Meaning | Risk |
|---|---|---|
[x] | Copy by value | Costly for large objects |
[&x] | Reference | Dangling if the lambda outlives x |
[=] / [&] | Everything by value / reference | Implicit, easy to capture more than intended |
[p = std::move(x)] | Init capture, move in | Preferred way to own a resource |
[this] | Captures the pointer, not the object | Dangling after the object is destroyed |
[*this] | Copies the object (C++17) | Safe across a deferred call, but copies |
A lambda stored in a std::function or returned from a function must not capture by reference anything that dies before it is called. That single mistake accounts for most use-after-free crashes in modern C++.
Algorithms and ranges pipelines
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
struct Person { std::string name; int age; };
int main() {
std::vector<Person> people{{"ada", 36}, {"grace", 45}, {"alan", 41}, {"alan", 12}};
namespace rv = std::views;
// C++20 ranges: pass the range, not two iterators
std::ranges::sort(people, {}, &Person::age); // projection sorts by age
// a lazy pipeline: nothing is copied and nothing runs until the loop
auto pipeline = people
| rv::filter([](const Person& p) { return p.age > 18; })
| rv::transform([](const Person& p) { return p.name; })
| rv::take(2);
for (const auto& name : pipeline) std::cout << name << '\n';
// materialise when you need to own the result
std::vector<std::string> adults(people.size());
auto out = std::ranges::copy_if(people, adults.begin(),
[](const Person& p) { return p.age >= 18; },
&Person::name); // projection again
adults.resize(static_cast<std::size_t>(out.out - adults.begin()));
}- Views are lazy and non-owning. They must not outlive the container they refer to.
- A view holds a reference to its source;
auto v = people | rv::filter(f);is fine only whilepeoplelives and is not modified. std::views::reverseneeds a bidirectional range;std::views::filteris not bidirectional, so filter-then-reverse does not compile. Reverse first.rv::iota(0, 10)andrv::transformlet you generate sequences without materialising them.- Prefer the ranges algorithm when it exists: it returns a result struct with the output iterator, and it constrains its arguments.
Iterator invalidation
std::vector<int> v{1, 2, 3, 4, 5};
// WRONG: erase invalidates every iterator from the erase point onward
for (auto it = v.begin(); it != v.end(); ++it)
if (*it % 2 == 0) v.erase(it);
// right: erase returns the next valid iterator
for (auto it = v.begin(); it != v.end(); )
it = (*it % 2 == 0) ? v.erase(it) : it + 1;
// better: one pass, no quadratic shifting
std::erase_if(v, [](int x) { return x % 2 == 0; });
// reserve removes reallocation from a push_back loop
std::vector<int> out;
out.reserve(v.size());
for (int x : v) out.push_back(x * 2);💡
Reallocation invalidates every pointer, reference and iterator into a
vector. Anything holding an element reference across a push_back is a latent bug; use indices, or a container with stable references such as std::deque or std::list.FAQ
Are ranges slower than raw loops?
Usually the same after optimisation, because the view pipeline inlines into the loop. Where they differ is compile time: deeply composed views instantiate a large amount of template machinery. Measure before assuming either.
When should I use std::function instead of an auto lambda?
Only when you must type-erase: storing heterogeneous callables in one container, or a callback that crosses a library boundary.
std::function adds an indirection and may allocate, so keep auto or a template parameter in hot paths.Related
Templates, concepts and generic programming The standard library and smart pointers
Last refreshed 2026-09-18.