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; };
}
CaptureMeaningRisk
[x]Copy by valueCostly for large objects
[&x]ReferenceDangling if the lambda outlives x
[=] / [&]Everything by value / referenceImplicit, easy to capture more than intended
[p = std::move(x)]Init capture, move inPreferred way to own a resource
[this]Captures the pointer, not the objectDangling 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 while people lives and is not modified.
  • std::views::reverse needs a bidirectional range; std::views::filter is not bidirectional, so filter-then-reverse does not compile. Reverse first.
  • rv::iota(0, 10) and rv::transform let 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.

Templates, concepts and generic programming The standard library and smart pointers

Last refreshed 2026-09-18.