The problem is combinatorial. You have N algorithms (sort, search, find, copy) and M containers (array, list, tree, hash table). The naive approach: implement each algorithm for each container. That’s N×M implementations.
The insight is to interpose an abstraction layer.
The Iterator Abstraction
Instead of algorithms knowing about containers directly, we define iterator categories—capabilities that algorithms require and containers provide:
Input: Single-pass read. You can advance (++) and dereference (*), but once you move forward, you can’t go back. Stream-like.
Forward: Multi-pass. You can iterate multiple times; begin() always gives the same starting point.
Bidirectional: Can go backward (--). Enables algorithms like reverse iteration.
Random-access: Can jump anywhere (+n, []). Enables binary search, sorting.
A True Input Iterator
The input iterator category exists for a reason. Here’s a working example that reads entropy from /dev/urandom:
#include <fstream>
#include <iterator>
#include <cstdint>
struct entropy_iterator {
using iterator_category = std::input_iterator_tag;
using value_type = uint8_t;
using difference_type = std::ptrdiff_t;
using pointer = const uint8_t*;
using reference = uint8_t; // returns by value, not reference
std::ifstream* source = nullptr;
uint8_t byte = 0;
entropy_iterator() = default; // sentinel (end iterator)
explicit entropy_iterator(std::ifstream& s) : source(&s) {
++(*this); // prime the first byte
}
uint8_t operator*() const { return byte; }
entropy_iterator& operator++() {
if (source && source->good()) {
source->read(reinterpret_cast<char*>(&byte), 1);
if (!source->good()) source = nullptr;
}
return *this;
}
entropy_iterator operator++(int) {
auto tmp = *this;
++(*this);
return tmp;
}
bool operator==(const entropy_iterator& other) const {
return source == other.source;
}
};
Use it like any input iterator:
int main() {
std::ifstream urandom("/dev/urandom", std::ios::binary);
entropy_iterator it(urandom);
// generate 16 random bytes
std::vector<uint8_t> key(16);
std::copy_n(it, 16, key.begin());
// or use in algorithms
int sum = 0;
for (int i = 0; i < 1000; ++i, ++it) {
sum += *it;
}
// sum ≈ 127500 (mean of uniform [0,255] × 1000)
}
Each ++ consumes a fresh entropy byte from the kernel—you literally cannot iterate twice over the same sequence. This is why the input iterator category exists: some sources are inherently single-pass. Claiming forward iterator capabilities would be a lie.
The same pattern applies to network streams, sensor readings, and any source where data is consumed by reading it.
The Payoff
Now binary_search doesn’t need to know about vectors, deques, or sorted arrays. It only needs random-access iterators. The algorithm expresses its requirements; the container provides capabilities. They compose through the iterator abstraction.
N algorithms + M containers = N+M implementations.
Why This Matters
This is Abelson and Sussman’s abstraction barrier from SICP, applied to C++. The iterator concept creates a clean separation: algorithms above, containers below, a stable interface between.
Stepanov understood that generic programming isn’t about making things work with any type—it’s about identifying the minimal requirements for each algorithm and expressing them precisely. The iterator hierarchy is that precision.
Every time you write std::sort(v.begin(), v.end()), you’re benefiting from this N+M composition.
The best abstractions don’t just reduce code—they reduce the complexity of thought required to reason about systems.
Discussion