Worked examples — Iterators — input, output, forward, bidirectional, random access, contiguous
Before anything else, two words we lean on constantly:
The scenario matrix
Every iterator exercise is one of these cells. We will hit each cell with a labelled example. In the "Negative move" cell we require an iterator that can step backward — that means the bidirectional, random access, or contiguous category (the three that define --it).
| Cell | What makes it special | Degenerate / limit inside it |
|---|---|---|
| A. Positive move, random access | it += n, one jump |
n = 0 (no move) |
| B. Positive move, bidirectional | must loop ++it, |
reaching end() exactly |
| C. Negative move | --it, legal only on bidirectional / random access / contiguous |
must not step before begin() |
| D. Single-pass input | read once, no going back | copy-then-advance invalidates the other |
| E. Output only | write-only, no ==, no end |
knowing how many to write |
| F. Contiguous vs merely random | &*(it+n) == &*it + n |
deque breaks it |
| G. Empty / one-element range | begin() == end() |
distance ; --end() illegal |
| H. Word problem | pick the minimum category | over-constraining is a design bug |
| I. Exam twist | which line fails to compile & why | tag-dispatch reasoning |
Example 1 — Cell A: positive jump on a vector (and the n = 0 limit)
v.begin()points at index 0, value10. Why this step? Every walk needs a known starting anchor;begin()is index 0 by definition.advance(it, 3)on a random-access iterator becomesit += 3— a single jump to index 3. Why this step? The parent's tag dispatch picks therandom_accessoverload forvector. Because a vector is one flat block, address arithmetic finds index 3 without touching indices 1 or 2 — that is why the cost stays constant () no matter how big the vector is. Index , value40. Soa = 40.advance(it, 0)moves zero steps — the loop body never runs /it += 0is a no-op. Why this step? The degeneraten = 0must leave the iterator exactly where it was;b = *it = 40.
Verify: index arithmetic ;
v[3] = 40, soa == b == 40. Distance frombegin()to finalitis3. ✓
Example 2 — Cell B + G limit: walking a list to end() exactly
listiterators are only bidirectional, soadvancepicks the looping overload:while(n--) ++it;. Why this step? A linked list has no contiguous block, so there is no+=; you must physically hop node by node — and since moving steps costs separate hops, the work grows in step with : this is list's (linear) reality.- Loop runs for
n = 3:++itexecutes 3 times, landing one past the last element. Why this step? A container of size 3 has valid positions and the sentinelend()at "position 3". Stepping 3 times frombegin()reaches exactlyend(). it == L.end()istrue. Why this step? This is the limit case:end()is the legal stopping point. Reading*ithere would be undefined, but comparing is fine.
Read the figure like this: the three mint boxes on the bottom row are the real elements at positions 0, 1, 2 holding 7, 8, 9. The butter box on the far right, labelled end() at position 3, is the sentinel — notice it has no value inside, which is your visual reminder that dereferencing it is illegal. The lavender arrow dropping onto position 0 is where begin() starts. Now count the three curved coral arrows across the top: each is one ++. Trace them left to right and watch the third arrow land squarely on the empty end() box — that landing is the whole answer. It shows both that exactly three ++ calls ran (three arrows) and why atEnd is true (the last arrow reaches the sentinel, not past it).

Verify: number of
++calls size ofL; distancebegin()→end()equals size, soatEnd == true. ✓
Example 3 — Cell C: negative move on bidirectional, guarding begin()
setis bidirectional, so negativenpickswhile(n++) --it;. Why this step? Only bidirectional-or-better iterators define--. Astd::setis stored as a balanced binary search tree, and--itwalks to the in-order predecessor node. That walk may climb up or down the tree, so its worst-case cost is proportional to the tree height, which is for a balanced tree — cheap, but not the that a vector gives. (A full front-to-back traversal visits every node exactly once, so it still totals ; the point is a single--is bounded by tree height, not constant.) Either way--is legal here, which is all Cell C needs.end()is one past the last element35. First--itlands on35. Why this step? You must never dereferenceend(), but you may decrement it — the first--moves onto the real last element.- Second
--itlands on25. Why this step? Two backward steps from the sentinel:35→25. Soval = 25. - Degenerate guard: we stopped with room to spare. Had we written
advance(it, -5)fromend()of a 4-element set, we'd step beforebegin()— undefined behaviour. Why this step? Cell C's hidden trap: backward moves must not underflow the range; the standard gives no safety net.
Verify: sorted set
{5,15,25,35}; two steps back fromend()→ element at indexsize - 2 = 2→ value25. ✓
Example 4 — Cell D: single-pass input, copy invalidates
istream_iteratoris an input iterator: single-pass. Why this step? It is a thin wrapper over a stream that has already consumed characters — there is no rewind, matching how its `iterator_category` isinput_iterator_tag.*itreads100but does not remove it yet;first = 100. Why this step? The first token was buffered when the iterator was constructed.++itconsumes the next token from the shared stream. Why this step? Bothitandcopysit on the same underlyingss. Advancing one moves the stream for both — copies are not independent for input iterators.*copyafter the advance is not safe to rely on. Why this step? The Standard's input-iterator requirements (theCpp17InputIteratorrules, [input.iterators]) guarantee validity only for a single pass: once you increment any copy of an input iterator, all other copies are invalidated, and dereferencing an invalidated iterator is undefined behaviour — meaning the program has no defined meaning at all (it may print200, print garbage, or crash). This is stronger than unspecified behaviour, where you'd at least get some valid value chosen from a known set. The safe conclusion is: never read a copy after advancing another. Only forward iterators upgrade this to multi-pass, where copies are independent.
Verify: the checkable facts are
first == 100, and that the category is input (single-pass), so multi-pass reasoning aboutcopyis invalid. ✓
Example 5 — Cell E: output-only, you must count writes
back_inserteris a pure output iterator. Why this step?*out = xis defined to meandst.push_back(x); there is no==, no end sentinel. You cannot ask "am I done?" — you must know the count yourself (here, 4 loop items).- Each
*out = x; ++out;appends one element. Why this step? Output iterators support exactly*it = vand++it; the++is a formality (it's a no-op for back-inserters) that keeps the algorithm's shape uniform. - After 4 writes,
dst = {1,2,3,4}, son = 4. Why this step? The number written equals the number of source items — the only "length" information available.
Verify: 4 writes into an empty vector give
dst.size() == 4anddst == {1,2,3,4}. ✓
Example 6 — Cell F: contiguous keeps its promise, deque does not
vectoris contiguous, so its elements sit in one block: address of element isbase + k * sizeof(double). Why this step? Contiguity is the one guarantee above random access — see the parent's table row "contiguous storage guarantee".&*(v.begin()+2)is the address ofv[2];&*v.begin() + 2is base address plus 2 elements. Why this step? We are comparing "walk the iterator then take the address" vs "take the base address then add 2" — contiguity says these are identical. SovecOK = true.- A
dequewouldit[2]in (random access) yet may store element 2 in a different chunk. Why this step? Random access is necessary but not sufficient for pointer arithmetic. The address equality can silently fail ondeque, which is why you may hand&v[0](contiguous) but never&deque[0]to a C API expecting a flat array — a fact that flows from pointer arithmetic.
Read the figure like this: compare the two rows box-for-box. The top lavender row is the vector. Its four boxes v[0]..v[3] sit edge to edge with no gaps, and the small grey labels beneath them read base+0, base+1, base+2, base+3 — perfectly linear. Put your finger on v[2] and check its label: it is base+2, so the mint "✓" on the right is earned. The bottom row is the deque: two coral boxes (d[0], d[1]), then a labelled gap (the visual cue that a new memory chunk starts), then two butter boxes (d[2], d[3]). Now put your finger on d[2]: it lives across the gap, so its real address is not base+2 — that broken linearity is exactly what the coral "✗" warns about. The single difference between the two rows — gap vs no gap — is the entire reason only contiguous storage keeps the base + n identity.

Verify: for a contiguous
doublearray,&a[2] == a + 2is exactly the pointer identity;vecOK == true. ✓
Example 7 — Cell G: empty and single-element ranges
- For an empty container
begin() == end(). Why this step? There is nowhere to point, so both sentinels coincide; the distance between equal iterators is0. Thusd0 = 0. - For one element,
end()is one step past index 0. Why this step? Distance = number of++frombegin()toend()= size =1. Sod1 = 1. - Degenerate trap: on the empty vector,
--empty.end()is undefined — there is no element to land on. Why this step? The limit case: you may decrementend()only whensize >= 1. This is Cell G's hidden edge every exam loves.
Verify:
distance == sizefor a forward-or-better range, sod0 == 0,d1 == 1. ✓
Example 8 — Cell H: word problem, pick the minimum category
- List the operations you actually use:
*it(read),++it,it != Last. Why this step? The right category is decided by operations used, not by habit — over-constraining rejects valid containers. - You never go backward, never jump, read each element once. Why this step? No
--, no+n: bidirectional and random access are unnecessary. Single-pass read +!=is exactly the input iterator contract. - Minimum = input iterator; passes = 1. Why this step? Widening the accepted set from random-access down to input lets
forward_list,istream, everything work — the whole point of the algorithm/iterator decoupling. The teammate'srandom_accessconstraint was an over-specification bug.
Verify: operations used = {read,
++,!=}; the least category providing all three is input (multi-pass not even required), so minimum category index = 1 (input), passes = 1. ✓
Example 9 — Cell I: exam twist, which line fails and why
- Line X uses
a + 2. Why this step? The+operator on iterators exists only for random-access (and contiguous) iterators. Alistiterator is bidirectional, so it defines++/--but notoperator+. There is no valid overload fora + 2, so the compiler rejects line X. This is the failing line. - Line Y
std::advance(a, 2)compiles fine. Why this step?advanceis defined for all iterator categories via tag dispatch; for a bidirectional iterator it silently falls back to looping++atwice (). It never asks foroperator+, so it succeeds where line X could not — this is precisely the parent note'sadvanceworkaround. - Line Z
std::distance(L.begin(), a)also compiles. Why this step?distancelikewise dispatches on category and, for bidirectional, counts++steps; it needs nooperator-. So Z is fine too. - Delete line X and run. After
advance(a, 2),asits at index 2 (value6), anddistance(begin(), a)counts 2 steps. Why this step? This confirms the intended answer: only line X fails, and once removed,gap = 2with*a == 6.
Verify: removing the illegal
+,advance(a,2)putsaat index 2, sogap == 2and*a == 6. ✓
Active recall
Recall Quick self-test
On a vector, advance(it, 0) does what? ::: Nothing — the degenerate zero move leaves the iterator unchanged.
Two -- from set::end() of {5,15,25,35} gives which value? ::: 25 (index size−2).
Why is reading a stale copy of an istream_iterator unsafe? ::: Input iterators are single-pass; incrementing any copy invalidates all others, and dereferencing an invalidated iterator is undefined behaviour.
distance(begin(), end()) for an empty container equals? ::: 0, because begin() == end().
Which iterator category is the minimum to print a range once? ::: Input iterator.
Why does L.begin() + 2 fail for a std::list? ::: operator+ exists only for random-access/contiguous; list iterators are bidirectional, so they have no +.
What is the worst-case cost of a single --it on a std::set? ::: — bounded by the balanced tree's height, not constant.
deque supports it[n] in O(1) — is &*(it+n) == &*it + n guaranteed? ::: No; that address identity needs the contiguous category, which deque lacks.