5.2.21 · D4C++ Programming

Exercises — Iterators — input, output, forward, bidirectional, random access, contiguous

2,774 words13 min readBack to topic

Reference figures

The first figure is the memory picture — the single fact that separates vector (contiguous) from deque (random access but chunked) from list (bidirectional, scattered). Keep it in view for L3–L5.

Figure — Iterators — input, output, forward, bidirectional, random access, contiguous

The second figure is the capability ladder — each rung adds exactly one new operation on top of the rung below. Refer to it whenever a problem asks "which category introduces X?"

Figure — Iterators — input, output, forward, bidirectional, random access, contiguous

L1 — Recognition

Recall Solution 1.1

(a) std::vector<int>Contiguous — elements sit in one memory block. (b) std::list<int>Bidirectional — a doubly-linked list can step ++ and --, but cannot jump. (c) std::deque<int>RandomAccessit + n is , but memory is chunked, so not contiguous. (d) std::set<int>Bidirectional — it is a balanced tree; you can walk both directions but not index. (e) std::forward_list<int>Forward — singly-linked, so it can only step forward and re-traverse.

Recall Solution 1.2

(a) std::istream_iterator<int>Input — read-only, single-pass; each ++ pulls one token off the stream. (b) std::ostream_iterator<int>Output — write-only; *it = x prints, ++it is a no-op. (c) std::back_insert_iteratorOutput*it = x calls push_back(x) on the container; no reading, no equality.

Recall Solution 1.3
  • --itBidirectional (backward stepping).
  • it[n]RandomAccess ( indexing).
  • *it = v write → Output (writing is the defining power of output; Forward keeps it).
  • multi-pass re-read → Forward (you may copy and revisit).
  • &*(it+n) == &*it + nContiguous (linear address guarantee).

L2 — Application

Recall Solution 2.1

(a) vector (random access): it += 5; — a single pointer jump. (b) list (bidirectional): std::advance(it, 5); — internally loops ++it five times, . it += 5 won't compile. (c) istream_iterator (input): std::advance(it, 5); — loops ++it, consuming 5 tokens, , single-pass. You can never go back.

std::advance is the safe choice everywhere; it uses tag dispatch (see Templates and Tag Dispatch) to pick vs automatically.

Recall Solution 2.2

(a) v.end() - v.begin() = 10, computed in : random-access iterators subtract like pointers. (b) L.end() - L.begin() fails because subtraction needs random access. Use std::distance(L.begin(), L.end()) = 10, computed in by counting ++ steps.

Recall Solution 2.3
  • std::sort(v.begin(), v.end()); — works because std::sort requires random access (it jumps around while partitioning).
  • L.sort(); — the free std::sort won't compile on L (bidirectional only), so list ships its own member sort that rewires node pointers instead of jumping.

L3 — Analysis

Recall Solution 3.1

(A) compiles because vector::iterator is contiguous → random access, so it overloads operator+(iterator, int). *a == 30 (the element at index 2). (B) fails because list::iterator is bidirectional: it defines ++ and -- but not operator+. There is no way to jump n steps in over scattered nodes, so the language does not even provide the syntax. The correct rewrite is auto b = L.begin(); std::advance(b, 2);.

Recall Solution 3.2

(A) c_api(&v[0], 4) is safe. vector is contiguous: its four doubles occupy one linear block, so &v[0] followed by +1, +2, +3 lands on the real elements — exactly &*(it+n) == &*it + n. (B) c_api(&d[0], 4) is undefined behaviour. deque is random-access but chunked (figure 1, right): elements 0–1 may live in one block and 2–3 in another. &d[0] + 2 points into the first block's off-the-end area, not at d[2]. Random access ≠ contiguous.

Recall Solution 3.3

y reads whatever the stream now offers after the original advanced — the copy is invalidated, so *copy does not reliably give back 5. In practice it yields the same value the original now sees (undefined/unspecified to rely on it). This demonstrates that input iterators are single-pass: advancing any iterator over a stream moves the shared underlying stream, so copies do not preserve position. Multi-pass (safe copying and re-reading) begins only at Forward.


L4 — Synthesis

Recall Solution 4.1
template<class It>
void impl(It& it, int n, std::random_access_iterator_tag) {
    it += n;                          // O(1)
}
template<class It>
void impl(It& it, int n, std::bidirectional_iterator_tag) {
    if (n >= 0) while (n--) ++it;     // O(n) forward
    else        while (n++) --it;     // O(|n|) backward
}
template<class It>
void impl(It& it, int n, std::input_iterator_tag) {
    while (n--) ++it;                 // O(n), forward only, single-pass
}
template<class It>
void my_advance(It& it, int n) {
    impl(it, n,
         typename std::iterator_traits<It>::iterator_category{});
}
  • n = 0: every overload does nothing (it += 0, or the while never runs). Correct no-op.
  • negative n on bidirectional: the else branch runs --it exactly |n| times. On an input iterator negative n is ill-formed (no --), which the tag system correctly refuses to compile.

See iterator_traits and type introspection for how iterator_category is extracted.

Recall Solution 4.2
std::copy(std::istream_iterator<int>(std::cin),   // INPUT source
          std::istream_iterator<int>(),           // end sentinel
          std::ostream_iterator<int>(std::cout, " ")); // OUTPUT sink

Why it works: std::copy needs only an input iterator for the source (read + ++) and an output iterator for the destination (*it = x + ++). Both are single-pass, which is all copy requires. See STL Algorithms — sort, find, copy.

Recall Solution 4.3

The standard tool is std::distance, which is itself tag-dispatched:

template<class It>
auto steps_to_end(It begin, It end) {
    return std::distance(begin, end);   // O(1) if random access, else O(n)
}
  • std::vector<int> v(7): steps_to_end(v.begin(), v.end()) = 7, computed in (subtraction).
  • std::forward_list<int> f{1,1,1}: = 3, computed in (counting ++).

L5 — Mastery

Recall Solution 5.1

Each iteration calls std::advance(it, i) on a bidirectional iterator, costing . Total: For that is steps. Fix — walk once, :

for (auto it = L.begin(); it != L.end(); ++it)
    use(*it);
Recall Solution 5.2
Algorithm Weakest category needed Reason
std::copy Input (source) + Output (dest) single forward pass
std::find Input linear scan, read + ++
std::reverse Bidirectional swaps from both ends inward, needs --
std::binary_search Random access for (works on Forward but degrades to walking) needs midpoint jumps
std::sort Random access partition jumps around

This is why std::list (bidirectional) can do find/reverse but not sort.

Recall Solution 5.3

std::vector<bool> is a specialization that packs each element into a single bit, not a byte. So v[i] returns a proxy object (a bit reference), and there is no addressable bool to take the address of — &v[0] doesn't even yield bool*. Its iterator is therefore not contiguous (arguably not even a true random-access-to-bool). For a real contiguous bool buffer use std::vector<char>, std::array<bool, N>, or std::deque-free flat storage.

Recall Solution 5.4

No, iterators cannot be contiguous. Requirement (i) — cheap growth at both ends — forces the storage into separate blocks (you cannot keep one flat array and still insert at the front in ). But (ii) indexing across those blocks is achievable with a block map. So the strongest category is RandomAccess (but not Contiguous) — exactly what std::deque provides. This is the design behind figure 1's middle panel.


Recall Self-test summary (reveal after finishing)

Which container is random-access but not contiguous? ::: std::deque. Total cost of "restart from begin and advance i" for i=0..n-1 on a list? ::: , specifically . Weakest category std::reverse needs? ::: Bidirectional (it needs --). Why is &vector<bool>[0] not a bool*? ::: It packs bits; [] returns a proxy, no addressable byte-sized bool. When is tag dispatch resolved? ::: At compile time, by the tag's type — zero runtime cost.