Phase 3 · Generic Programming & the Standard LibraryModule 16~58 min read

Iterators, Algorithms & Ranges

Express data processing with iterator ranges, standard algorithms, projections, and lazy C++20 range pipelines.

What you'll learn

Iterators separate traversal from storage, allowing one algorithm to work across many containers. C++20 ranges bring the range itself into the interface and add lazy, composable views. You will replace incidental loops with operations that state intent.

By the end, you'll be able to:

  • Explain half-open iterator ranges and iterator categories
  • Use standard search, transform, partition, and sorting algorithms
  • Apply projections and understand algorithm preconditions
  • Compose lazy C++20 views without creating dangling ranges

Iterators and half-open ranges

An iterator identifies a position and supports operations determined by its category. A half-open range [first, last) includes the first position and excludes the end sentinel. Empty ranges have first == last, adjacent ranges compose cleanly, and distance equals the element count for random-access iterators.

CategoryAddsExample source
InputSingle-pass readingInput stream iterator
ForwardMulti-pass traversalforward_list
BidirectionalMove backwardlist, map
Random accessConstant-time jumps and differencedeque
ContiguousAdjacent elements occupy adjacent memoryvector, array, span

Key idea

An algorithm's iterator requirement is a contract. Sorting needs random access; a linked-list iterator cannot support the necessary constant-time jumps.

Search and count

Algorithms name recurring intent: find a value, locate the first matching element, count a condition, or test whether all elements satisfy a predicate. The result is often an iterator; compare it with the range end before dereferencing.

search.cpp
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector temperatures{18, 21, 27, 24, 31};
    auto hot{std::ranges::find_if(temperatures, [](int value) {
        return value >= 30;
    })};

    if (hot != temperatures.end()) {
        std::cout << "first hot reading: " << *hot << '\n';
    }
    std::cout << "above 20: "
              << std::ranges::count_if(temperatures, [](int v) { return v > 20; })
              << '\n';
}

Transform and accumulate

transform maps input elements into an output range. The destination must have enough existing elements or use an insertion iterator such as back_inserter.accumulate folds values from an explicit initial type, which also determines the result type.

transform.cpp
#include <algorithm>
#include <iostream>
#include <iterator>
#include <numeric>
#include <vector>

int main() {
    std::vector prices{10.0, 25.0, 40.0};
    std::vector<double> taxed;
    taxed.reserve(prices.size());

    std::ranges::transform(prices, std::back_inserter(taxed),
                           [](double price) { return price * 1.1; });
    const double total{std::accumulate(taxed.begin(), taxed.end(), 0.0)};
    std::cout << total << '\n';
}

Watch out

Using integer 0 as the initial value of a floating-point accumulation makes the accumulator an integer. Use 0.0 or another intentional result type.

Sort, partition, and preconditions

Sorting rearranges elements according to a strict weak ordering. Partitioning groups elements by a predicate without necessarily sorting within each group. Many algorithms have preconditions: binary_search requires compatible sorted order, and violating such a precondition makes the result invalid or behavior undefined depending on the contract.

sort_projection.cpp
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

struct Student { std::string name; int score; };

int main() {
    std::vector<Student> students{{"Maya", 88}, {"Noah", 95}, {"Lina", 91}};
    std::ranges::sort(students, std::greater{}, &Student::score);

    for (const auto& student : students) {
        std::cout << student.name << ' ' << student.score << '\n';
    }
}

Note

A projection selects the compared part—in this case Student::score—without wrapping the entire comparison in a custom lambda.

Ranges algorithms

Ranges algorithms accept a whole range, reducing mismatched begin/end errors. Their results often return structured information, and constraints reject unsupported operations close to the call site. They remain eager: a call such as ranges::sort performs work immediately.

ranges.cpp
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector values{7, 2, 7, 4, 2, 9};
    std::ranges::sort(values);
    auto new_end{std::ranges::unique(values).begin()};
    values.erase(new_end, values.end());

    for (int value : values) std::cout << value << ' ';
}

Lazy views and pipelines

A view is a lightweight, usually non-owning transformation evaluated as it is iterated. Filtering and mapping can compose with the pipe operator without allocating intermediate containers. Because views often borrow, the source must stay alive and structurally valid.

views.cpp
#include <iostream>
#include <ranges>
#include <vector>

int main() {
    std::vector values{1, 2, 3, 4, 5, 6};
    auto squares_of_even = values
        | std::views::filter([](int value) { return value % 2 == 0; })
        | std::views::transform([](int value) { return value * value; });

    for (int value : squares_of_even) std::cout << value << ' ';
}

Tip

Materialize a view into an owning container when the result must outlive its source, cross an API boundary with uncertain lifetime, or be evaluated once and stored.

Recap & quick check

Key takeaways

  • Half-open iterator ranges make empty ranges, composition, and end positions consistent.
  • Algorithms state search, transformation, partition, and sorting intent more clearly than incidental loops.
  • Algorithm preconditions and iterator-category requirements are correctness contracts.
  • Ranges algorithms accept whole ranges and offer constrained, structured interfaces.
  • Views are lazy and often borrowed, so their source lifetime and invalidation still matter.

Quick check

1. What positions does [first, last) include?

2. What must you do before dereferencing a find result?

3. Why can accumulate(values, 0) be wrong for doubles?

4. When does a lazy transform view compute an element?

Next: Module 17 — Lambdas, Callables & Functional Tools, where behavior itself becomes a reusable value.