Custom Iterators
Implementing correct C++ iterators—forward, bidirectional, random access, and C++20 ranges-compatible with sentinel types.
Custom Iteratorsince C++98A user-defined type that models one of the standard iterator concepts, enabling it to participate in range-based for loops, standard algorithms, and (since C++20) the ranges library.
Overview
Every iterator must expose associated types and a set of operators determined by its category. Pre-C++20 algorithms dispatch on iterator_category queried through std::iterator_traits. C++20 replaced this with named concepts (std::input_iterator, std::forward_iterator, etc.) and added iterator_concept as the opt-in tag for the new system. Both coexist: define iterator_category for legacy algorithm compatibility and iterator_concept for ranges.
The five required associated types are value_type, difference_type, reference, pointer, and iterator_category. C++20 adds iterator_concept, which may differ from iterator_category—a contiguous iterator sets iterator_concept = std::contiguous_iterator_tag while keeping iterator_category = std::random_access_iterator_tag for ABI-safe legacy dispatch. std::iterator<> was the helper for these typedefs until C++17 deprecated it; define the types directly.
Iterator Categories
| Category | Requires | C++20 concept | Example |
|---|---|---|---|
| Input | *it, ++it, == (single-pass) | std::input_iterator | std::istream_iterator |
| Forward | Input + multi-pass guarantee | std::forward_iterator | std::forward_list::iterator |
| Bidirectional | Forward + --it | std::bidirectional_iterator | std::list::iterator |
| Random Access | Bidirectional + it+n, it[n], <=> | std::random_access_iterator | std::deque::iterator |
| Contiguous (C++20) | Random access + contiguous memory | std::contiguous_iterator | std::vector::iterator |
| Output | *it = val, ++it (no category hierarchy) | std::output_iterator | std::back_insert_iterator |
Syntax
struct MyIterator {
using value_type = int;
using difference_type = std::ptrdiff_t;
using reference = int&;
using pointer = int*;
using iterator_category = std::forward_iterator_tag; // pre-C++20 algorithms
using iterator_concept = std::forward_iterator_tag; // C++20 ranges
reference operator*() const;
pointer operator->() const;
MyIterator& operator++(); // pre-increment
MyIterator operator++(int); // post-increment; must return old value by value
bool operator==(const MyIterator&) const; // C++20: = default if members compare correctly
};operator!= is synthesised from operator== in C++20 and rarely needs explicit definition.
Examples
Forward Iterator: String Splitter
The end state is encoded in remaining_ and current_ being empty, avoiding a separate sentinel type while still satisfying the multi-pass guarantee.
class StringSplitter {
std::string_view str_;
char delim_;
public:
StringSplitter(std::string_view s, char d) : str_{s}, delim_{d} {}
struct iterator {
using value_type = std::string_view;
using difference_type = std::ptrdiff_t;
using reference = std::string_view; // returned by value; views are cheap
using pointer = const std::string_view*;
using iterator_category = std::forward_iterator_tag;
using iterator_concept = std::forward_iterator_tag; // C++20
std::string_view remaining_;
std::string_view current_;
char delim_ = ',';
iterator() = default; // end state: both empty
iterator(std::string_view s, char d) : remaining_{s}, delim_{d} { advance(); }
std::string_view operator*() const { return current_; }
const std::string_view* operator->() const { return ¤t_; }
iterator& operator++() { advance(); return *this; }
iterator operator++(int) { auto tmp = *this; ++*this; return tmp; }
bool operator==(const iterator& o) const {
return remaining_ == o.remaining_ && current_ == o.current_;
}
private:
void advance() {
if (remaining_.empty()) { current_ = {}; return; }
auto pos = remaining_.find(delim_);
if (pos == std::string_view::npos) {
current_ = remaining_; remaining_ = {};
} else {
current_ = remaining_.substr(0, pos);
remaining_ = remaining_.substr(pos + 1);
}
}
};
iterator begin() const { return {str_, delim_}; }
iterator end() const { return {}; }
};
static_assert(std::forward_iterator<StringSplitter::iterator>); // C++20
static_assert(std::ranges::forward_range<StringSplitter>); // C++20
for (auto tok : StringSplitter{"one,two,,four", ','})
std::println("{}", tok); // std::println: C++23; prints "one", "two", "", "four"Bidirectional Iterator
template<typename T>
class IntrList {
struct Node { T val; Node* prev = nullptr; Node* next = nullptr; };
Node* head_ = nullptr;
public:
template<bool IsConst>
struct IterImpl {
using value_type = T;
using difference_type = std::ptrdiff_t;
using reference = std::conditional_t<IsConst, const T&, T&>;
using pointer = std::conditional_t<IsConst, const T*, T*>;
using iterator_category = std::bidirectional_iterator_tag;
using iterator_concept = std::bidirectional_iterator_tag; // C++20
Node* node = nullptr;
reference operator*() const { return node->val; }
pointer operator->() const { return &node->val; }
IterImpl& operator++() { node = node->next; return *this; }
IterImpl operator++(int) { auto t = *this; ++*this; return t; }
IterImpl& operator--() { node = node->prev; return *this; }
IterImpl operator--(int) { auto t = *this; --*this; return t; }
bool operator==(const IterImpl&) const = default; // C++20
// implicit conversion: iterator → const_iterator
operator IterImpl<true>() const requires (!IsConst) { return {node}; }
};
using iterator = IterImpl<false>;
using const_iterator = IterImpl<true>;
iterator begin() { return {head_}; }
iterator end() { return {nullptr}; }
const_iterator begin() const { return {head_}; }
const_iterator end() const { return {nullptr}; }
};Random Access Iterator: Strided View
The symmetric n + it form requires a friend free function so argument-dependent lookup works regardless of operand order.
template<typename T>
class StrideView {
T* data_;
size_t stride_;
size_t count_;
public:
StrideView(T* d, size_t stride, size_t count)
: data_{d}, stride_{stride}, count_{count} {}
struct iterator {
using value_type = T;
using difference_type = std::ptrdiff_t;
using reference = T&;
using pointer = T*;
using iterator_category = std::random_access_iterator_tag;
using iterator_concept = std::random_access_iterator_tag; // C++20
T* ptr_ = nullptr;
size_t stride_ = 1;
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
T& operator[](difference_type n) const { return *(*this + n); }
iterator& operator++() { ptr_ += stride_; return *this; }
iterator operator++(int) { auto t = *this; ++*this; return t; }
iterator& operator--() { ptr_ -= stride_; return *this; }
iterator operator--(int) { auto t = *this; --*this; return t; }
iterator& operator+=(difference_type n) { ptr_ += n * (ptrdiff_t)stride_; return *this; }
iterator& operator-=(difference_type n) { ptr_ -= n * (ptrdiff_t)stride_; return *this; }
friend iterator operator+(iterator it, difference_type n) { return it += n; }
friend iterator operator+(difference_type n, iterator it) { return it += n; }
friend iterator operator-(iterator it, difference_type n) { return it -= n; }
difference_type operator-(const iterator& o) const {
return (ptr_ - o.ptr_) / static_cast<ptrdiff_t>(stride_);
}
bool operator==(const iterator&) const = default; // C++20
auto operator<=>(const iterator& o) const { return ptr_ <=> o.ptr_; }
};
iterator begin() { return {data_, stride_}; }
iterator end() { return {data_ + count_ * stride_, stride_}; }
};C++20 Ranges-Compatible Iterator with Sentinel
Separating the sentinel type from the iterator type lets end() return a lightweight marker with no per-element state. The only requirement is operator==(iterator, sentinel).
struct IntRange {
int first, last;
struct sentinel { int last; }; // carries end condition; not an iterator
struct iterator {
using value_type = int;
using difference_type = std::ptrdiff_t;
using iterator_concept = std::input_iterator_tag; // C++20; single-pass
int val;
int operator*() const { return val; }
iterator& operator++() { ++val; return *this; }
iterator operator++(int) { return {val++}; }
bool operator==(const iterator&) const = default;
bool operator==(sentinel s) const { return val == s.last; }
};
iterator begin() const { return {first}; }
sentinel end() const { return {last}; }
};
static_assert(std::ranges::range<IntRange>); // C++20
// std::default_sentinel_t is the standard generic sentinel tag (C++20)
struct LineReader {
std::istream& is;
struct iterator {
using value_type = std::string;
using difference_type = std::ptrdiff_t;
using iterator_concept = std::input_iterator_tag; // C++20
std::istream* is_ = nullptr;
std::string line_;
iterator() = default;
explicit iterator(std::istream& s) : is_{&s} { ++*this; }
const std::string& operator*() const { return line_; }
const std::string* operator->() const { return &line_; }
iterator& operator++() {
if (!std::getline(*is_, line_)) is_ = nullptr;
return *this;
}
iterator operator++(int) { auto t = *this; ++*this; return t; }
bool operator==(std::default_sentinel_t) const { return is_ == nullptr; }
};
iterator begin() { return iterator{is}; }
std::default_sentinel_t end() { return {}; }
};
LineReader reader{std::cin};
auto lines = reader | std::views::take(10); // C++20 ranges pipelineBest Practices
Assert concepts in tests. Static assertions catch broken iterator implementations at compile time, before any algorithm instantiates them:
static_assert(std::forward_iterator<StringSplitter::iterator>);
static_assert(std::ranges::forward_range<StringSplitter>);
static_assert(std::random_access_iterator<StrideView<int>::iterator>);Use the IsConst template trick for const/non-const iterator pairs. A single IterImpl<bool IsConst> with std::conditional_t for reference and pointer eliminates duplication. Add an implicit conversion IterImpl<false> → IterImpl<true> so iterator converts to const_iterator automatically—this is how std::vector::iterator behaves.
Prefer friend free functions for symmetric arithmetic. friend functions defined inside the class body are found by ADL without polluting the enclosing namespace.
Choose the narrowest iterator_concept that is honestly satisfied. A forward iterator guarantees multi-pass: copying an iterator and advancing one copy leaves the other unchanged. Stream-backed iterators cannot satisfy this—they are input-only. Overstating the category causes undefined behaviour in algorithms that rely on the multi-pass guarantee.
Common Pitfalls
Broken sentinel comparison. A sentinel operator== that always returns false creates an infinite range. The sentinel must encode the actual end condition:
// WRONG — infinite loop; sentinel comparison never fires
bool operator==(sentinel) const { return false; }
// CORRECT — sentinel carries the end value
bool operator==(sentinel s) const { return val == s.last; }Missing const overloads break const containers. Without begin() const returning const_iterator, calling begin() on a const MyRange fails to compile. Provide both iterator and const_iterator with matching const/non-const begin()/end() pairs.
Post-increment must return the prior value by value:
// WRONG — returns *this after advance; caller observing the return gets the new state
iterator& operator++(int) { ++*this; return *this; }
// CORRECT
iterator operator++(int) { auto tmp = *this; ++*this; return tmp; }Signed/unsigned mismatch in operator-. When computing iterator distance through a stride, dividing a signed pointer difference by an unsigned stride_ silently promotes to unsigned. Cast stride_ to ptrdiff_t before the division.
Invalidating reference when value_type is returned by value. If operator* returns value_type (not value_type&), then operator-> cannot return &operator*() because that takes the address of a temporary. Store the value in the iterator or return a proxy type that holds it.
C++20 Iterator Concepts
Concepts replace tag dispatch for constraint checking. Pre-C++20 code used static_assert(std::is_base_of_v<std::forward_iterator_tag, typename std::iterator_traits<It>::iterator_category>) to check iterator strength. C++20 named concepts are cleaner, composable, and produce better diagnostics.
The concept hierarchy mirrors the category hierarchy: each stronger concept refines the weaker ones via requires clauses. A type that satisfies std::random_access_iterator automatically satisfies std::bidirectional_iterator, std::forward_iterator, and std::input_iterator.
// C++20
#include <iterator>
template<std::input_iterator It>
void drain(It first, It last) {
// guaranteed: *first, ++first, first == last compile and are meaningful
while (first != last) process(*first++);
}
template<std::forward_iterator It>
void two_pass(It first, It last) {
// guaranteed: copying It and advancing one copy leaves the original intact
auto saved = first;
count_pass(first, last);
transform_pass(saved, last); // safe: multi-pass guarantee
}
template<std::random_access_iterator It>
void binary_search_range(It first, It last) {
// guaranteed: it + n, it - n, it[n], it1 - it2, it1 <=> it2 all compile
auto mid = first + (last - first) / 2;
if (*mid < target) first = mid + 1;
else last = mid;
}Constrain function templates at the call site with the weakest concept that the algorithm actually requires. This keeps the interface as open as possible while the body remains correct. Using std::random_access_iterator where only std::forward_iterator is needed excludes valid inputs unnecessarily.
// C++20
// Concept-constrained wrapper: verify your own iterators satisfy their intended category
static_assert(std::input_iterator<LineReader::iterator>);
static_assert(std::forward_iterator<StringSplitter::iterator>);
static_assert(std::bidirectional_iterator<IntrList<int>::iterator>);
static_assert(std::random_access_iterator<StrideView<int>::iterator>);
// contiguous_iterator additionally requires std::to_address(it) to return T*
// and iterator_concept = std::contiguous_iterator_tag
template<std::contiguous_iterator It>
void simd_process(It first, It last) {
T* raw = std::to_address(first); // pointer to first element
std::size_t n = last - first;
// hand off to SIMD or BLAS routine expecting a raw pointer + count
}Sentinel Types
A sentinel is a lightweight end-condition marker that is not itself an iterator. The only interface requirement between an iterator It and a sentinel Se is operator==(It, Se) — the sentinel does not need to be dereferenceable or incrementable. This enables end conditions that carry less state than the iterator itself.
std::default_sentinel_t is the standard library's generic sentinel tag, an empty type that pairs with any iterator that checks its own "exhausted" flag. Returning std::default_sentinel from end() signals that the iterator manages termination internally.
// C++20
// Null-terminated string sentinel: end is the '\0' character, not a pointer
struct NullTermSentinel {};
struct CStrIterator {
using value_type = char;
using difference_type = std::ptrdiff_t;
using iterator_concept = std::input_iterator_tag; // C++20
const char* ptr = nullptr;
char operator*() const { return *ptr; }
CStrIterator& operator++() { ++ptr; return *this; }
CStrIterator operator++(int) { auto t = *this; ++*this; return t; }
// Heterogeneous equality: iterator vs. sentinel
bool operator==(NullTermSentinel) const { return *ptr == '\0'; }
bool operator==(const CStrIterator& o) const = default;
};
struct CStrRange {
const char* str;
CStrIterator begin() const { return {str}; }
NullTermSentinel end() const { return {}; }
};
static_assert(std::ranges::range<CStrRange>); // C++20
// Sentinel avoids scanning for length upfront — no strlen() required
for (char c : CStrRange{"hello"}) std::print("{}", c);Heterogeneous sentinel comparison is permitted in range-based for since C++17 (before that, begin() and end() had to return the same type). C++20 makes it a first-class part of the ranges model via std::sentinel_for<Se, It>.
// C++20
// std::sentinel_for concept — checked automatically by std::ranges algorithms
static_assert(std::sentinel_for<NullTermSentinel, CStrIterator>);
static_assert(std::sentinel_for<std::default_sentinel_t, LineReader::iterator>);
// sized_sentinel_for additionally requires (sentinel - iterator) to give distance
// in O(1); enables std::ranges::size() without traversalRange-Based for Customization
Range-based for finds begin() and end() through a two-step lookup. The compiler first checks for member functions t.begin() and t.end(). If those are absent it falls back to ADL-found free functions begin(t) and end(t) in the associated namespaces. For types you own, member functions are the clearest choice; for types you cannot modify, the free-function ADL hook is the only option.
// C++11
// Free-function ADL hook for a third-party type you cannot modify
namespace thirdparty {
struct Buffer { unsigned char* data; std::size_t size; };
}
// ADL hook: must be in the same namespace as Buffer for lookup to find it
namespace thirdparty {
inline const unsigned char* begin(const Buffer& b) { return b.data; }
inline const unsigned char* end (const Buffer& b) { return b.data + b.size; }
}
thirdparty::Buffer buf{ptr, len};
for (unsigned char byte : buf) process(byte); // finds thirdparty::begin/end via ADLThe standard also defines std::ranges::begin and std::ranges::end as customisation-point objects (CPOs) that perform the same two-step lookup but are not injectable — they are called as std::ranges::begin(t), not found by ADL. Prefer them in generic range algorithms to get consistent semantics including const-correctness checks.
// C++20
// Customisation-point objects enforce stricter rules than raw ADL
template<std::ranges::range R>
void print_all(R&& r) {
auto first = std::ranges::begin(r); // CPO: tries member then ADL, requires valid iterator
auto last = std::ranges::end(r); // CPO: result is sentinel_for<decltype(first)>
for (; first != last; ++first)
std::cout << *first << '\n';
}For a type to participate in both the legacy range-based for and std::ranges algorithms, both begin()/end() members and the iterator_concept member typedef should be defined. Member begin() and end() should be const-qualified so const ranges work without additional overloads.
std::counted_iterator
std::counted_iterator wraps any iterator with a count, turning an unbounded range into a bounded one. It holds the wrapped iterator plus a remaining-count that decrements on each advance. When the count reaches zero it compares equal to std::default_sentinel, regardless of the underlying iterator's own end state.
// C++20
#include <iterator>
#include <algorithm>
// Wrap a pointer (or any iterator) so only the first N elements are visible
int data[] = {10, 20, 30, 40, 50, 60};
auto ci = std::counted_iterator{data, 3}; // iterator over first 3 elements
for (; ci != std::default_sentinel; ++ci)
std::cout << *ci << ' '; // prints: 10 20 30std::counted_iterator models the same concept as its wrapped iterator: wrapping a std::random_access_iterator produces a std::random_access_iterator; wrapping an std::input_iterator produces an std::input_iterator. This makes it composable with the full ranges algorithm suite.
// C++20
#include <ranges>
#include <algorithm>
std::vector<int> v{1, 2, 3, 4, 5, 6, 7, 8};
// std::views::counted builds a std::counted_iterator range from a pointer/iterator + count
auto first3 = std::views::counted(v.begin(), 3); // subrange of first 3 elements
std::vector<int> out;
std::ranges::copy(first3, std::back_inserter(out)); // out = {1, 2, 3}
// Useful when the source range is infinite or otherwise unbounded
auto naturals_from_10 = std::views::iota(10); // infinite range: 10, 11, 12, ...
auto ten_of_them = std::views::counted(
std::ranges::begin(naturals_from_10), 10);
std::ranges::for_each(ten_of_them, [](int n){ std::cout << n << ' '; });
// prints: 10 11 12 13 14 15 16 17 18 19std::counted_iterator exposes .count() to query the remaining elements and .base() to retrieve the underlying iterator. These are useful when you need to resume iteration from where the counted range ended or when an algorithm needs to read back the underlying position.
// C++20
auto ci2 = std::counted_iterator{v.begin() + 2, 4}; // starts at v[2], 4 elements
std::ranges::advance(ci2, 2); // advance by 2
std::cout << ci2.count(); // 2 — elements remaining
std::cout << *ci2.base(); // v[4] — underlying positionSee Also
std::iterator_traits— trait class queried by pre-C++20 algorithms to discover iterator associated typesstd::ranges::range(C++20) — concept a type must satisfy to be usable in ranges pipelinesstd::default_sentinel_t(C++20) — standard tag type usable as a generic sentinelstd::reverse_iterator— adaptor that wraps any bidirectional iterator and reverses directionstd::back_insert_iterator— reference output iterator; study it for output iterator idiomsstd::views::iota(C++20) — range factory for integer sequences with a canonical iterator implementation