The "cursor" that lets one algorithm work on every container — what begin()/end() really mean, why some algorithms reject some containers, and how C++20 ranges make it all readable.
An iterator is a generalized pointer — a cursor you advance with ++ and read with *. A range is just a pair [begin, end). Because every container speaks "iterator," one std::sort fits all — and C++20 ranges let you skip the begin()/end() boilerplate entirely.
From Lesson 1: containers and algorithms meet through iterators. An iterator behaves like a pointer — *it reads the element, ++it moves to the next. begin() points at the first element; end() points one past the last — a sentinel you never dereference.
[begin, end) convention means an empty range is just begin == end, and size is end - begin. It's everywhere in the library.// the explicit form — this is what range-for desugars into for (auto it = v.begin(); it != v.end(); ++it) std::cout << *it << " "; // *it reads the element // the sugar you actually write for (const auto& x : v) std::cout << x << " ";
Source: cppreference — Iterator library.
Not all iterators can do all things. A linked list's cursor can step forward and back but can't jump to "element 500"; a vector's can. The library grades iterators into categories, and each algorithm requires a minimum:
| Category | Can do | Containers |
|---|---|---|
| Input / Output | single-pass read or write, ++ | streams |
| Forward | multi-pass, ++ | forward_list, unordered_* |
| Bidirectional | + -- (step back) | list, map, set |
| Random-access | + it + n, it[n], jump anywhere | deque |
| Contiguous C++17 | + elements adjacent in memory | vector, array, string |
std::sort(myList...) won't compile
std::sort needs random-access iterators (it jumps around). std::list only offers bidirectional ones — so you use the member myList.sort() instead. When an algorithm "doesn't work" on a container, an iterator-category mismatch is usually why.Source: cppreference — Iterator categories.
find returns an iteratorMost "where is it?" functions return an iterator, or end() for "not found": auto it = find(v.begin(), v.end(), 7); if (it != v.end()) …
std::back_inserter(v) turns "write here" into "push_back onto v" — lets algorithms grow a destination. (Used with transform/copy, Lesson 8.)
cbegin, rbegincbegin() gives a read-only (const) iterator; rbegin() walks backwards. Same range idea, different cursor.
vector reallocation (Lesson 2), or erasing from one. Don't keep using an iterator after the container changed shape under it.Passing v.begin(), v.end() to every algorithm is noisy and lets you accidentally mix iterators from two containers. Ranges (C++20) let an algorithm take the whole container, and add views — lazy, composable transformations you pipe with |.
#include <algorithm> #include <ranges> std::vector<int> v = {5, 3, 1, 4, 2}; std::ranges::sort(v); // no begin()/end() — pass the container itself // views: lazily keep evens, then square them — composed left to right auto result = v | std::views::filter([](int n){ return n % 2 == 0; }) | std::views::transform([](int n){ return n * n; }); for (int x : result) std::cout << x << " "; // 4 16 (2², 4²)
filter | transform pipeline above does no work until the for loop pulls values through — and it never builds an intermediate vector. Read pipelines top-to-bottom like a Unix pipe.| Iterators (classic) | Ranges (C++20) | |
|---|---|---|
| Sort | std::sort(v.begin(), v.end()) | std::ranges::sort(v) |
| Find | std::find(v.begin(), v.end(), x) | std::ranges::find(v, x) |
| Transform+filter | nested loops or temp vectors | v | views::filter(..) | views::transform(..) |
| Mix-up risk | can pass a.begin(), b.end() by mistake | one argument — can't mismatch |
[begin, end) idea. The next lesson is the algorithms themselves — the verbs that act on these ranges.end() iterator point to?std::sort work on a vector but not a std::list?v | views::filter(...) is:std::find(v.begin(), v.end(), x) returns what when x is absent?*it reads the element, ++it advances, it == end() means done.[begin, end) mean?end() (one past the last). Empty range = begin == end; size = end - begin.sort needs random-access; a list gives only bidirectional, so std::sort rejects it (use list::sort).std::sort(v.begin(), v.end())?std::ranges::sort(v) — pass the container directly; no begin/end, no chance of mismatched iterators.views::filter, views::transform) piped with |. It computes on demand and builds no intermediate container.vector reallocation, or erasing elements. Don't reuse an iterator after the container changed shape.Try: "rewrite this raw iterator loop as a ranges pipeline", or "why is end() one-past-the-end and not the last element?" Paste a loop and I'll show you the algorithm that replaces it.