Last 30 Days
No notifications
An iterator is an object that points into a container and lets you move through it. Every STL container exposes begin() and end(). Algorithms talk to containers exclusively through iterators — that's why the same std::sort works on vector, array, raw arrays, and deque.
auto it = v.begin(); // get
*it; // dereference → element
++it; // advance one
it == v.end(); // compare
auto next = it + 3; // arithmetic (random-access only)[begin, end)end() points one past the last element. You can't dereference it, but you can compare against it.
for (auto it = v.begin(); it != v.end(); ++it)
cout << *it << ' ';#include <iterator>
auto n = distance(v.begin(), it); // index
auto mid = next(v.begin(), v.size() / 2);
auto p = prev(v.end(), 1);
advance(it, 5); // it += 5copy(src.begin(), src.end(),
back_inserter(dst)); // dst.push_back each
copy(src.begin(), src.end(),
front_inserter(dst)); // dst.push_front each
copy(src.begin(), src.end(),
inserter(dst, dst.begin())); // any positioncopy(istream_iterator<int>(cin),
istream_iterator<int>(),
back_inserter(v)); // read until EOFEvery iterator belongs to one of five categories, ordered weakest → strongest:
| Category | Operations | Example |
| Input | *it, ++it, == (read once) | istream_iterator |
| Output | *it = x, ++it (write once) | ostream_iterator, back_inserter |
| Forward | input + multi-pass | forward_list::iterator |
| Bidirectional | forward + --it | list, map, set |
| Random-access | bidirectional + it+n, it[n], it1 - it2, < | vector, array, deque, raw pointer |
C++20 added contiguous iterators (vector, array, string, span) — random-access plus the guarantee that elements are physically adjacent.
The category determines which algorithms can use the iterator. std::sort requires random-access. std::find only needs input. The compiler picks the most efficient implementation based on category.
begin(), end() — and Their cv-qualified Friendsv.begin(); v.end(); // iterator (mutable)
v.cbegin(); v.cend(); // const_iterator (read-only)
v.rbegin(); v.rend(); // reverse_iterator
v.crbegin(); v.crend(); // const reverse_iteratorThe non-member free functions std::begin(v), std::end(v) work on raw arrays too:
int arr[5] = {1, 2, 3, 4, 5};
sort(begin(arr), end(arr));for (auto it = v.rbegin(); it != v.rend(); ++it)
cout << *it << ' ';// Or simpler:
for (auto x : v | std::views::reverse) cout << x << ' '; // C++20
distance, advance, next, prevsize_t idx = distance(v.begin(), it); // O(1) for random-access, O(n) otherwiseauto m = next(v.begin(), v.size() / 2); // doesn't mutate begin()
advance(it, 3); // mutates it
auto p = prev(v.end(), 1); // last element
> Why use these? They work for *every* iterator category. it + n only compiles for random-access iterators (so it would break on list or map).
The classic algorithms write through an output iterator. To grow a container, wrap it with an inserter from :
vector<int> dst;
copy(src.begin(), src.end(), back_inserter(dst)); // push_back
list<int> lst;
copy(src.begin(), src.end(), front_inserter(lst)); // push_frontset<int> s;
copy(src.begin(), src.end(), inserter(s, s.end())); // generic insert
back_inserter(dst) returns a back_insert_iterator whose assignment calls dst.push_back.
Treat input/output streams as ranges:
#include <iterator>// Read all whitespace-separated ints from cin into a vector
vector<int> v{istream_iterator<int>(cin),
istream_iterator<int>()};
// Write a vector to cout, separated by spaces
copy(v.begin(), v.end(),
ostream_iterator<int>(cout, " "));
Combine with algorithms for some surprisingly readable I/O:
// Print all even numbers from input
copy_if(istream_iterator<int>(cin),
istream_iterator<int>(),
ostream_iterator<int>(cout, "\n"),
[](int x){ return x % 2 == 0; });Many container operations invalidate existing iterators (and references and pointers). Common cases:
| Container | Operation | What's invalidated |
vector | push_back triggers reallocation | ALL iterators |
vector | erase(it) | it and everything after |
vector | insert(it, x) (with realloc) | ALL iterators |
map, set | erase(it) | only it |
map, set | insert(...) | none |
unordered_map | rehash on insert | ALL iterators (refs to elements OK) |
// ❌ Undefined behaviour — ++it on an invalidated iterator
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it < 0) v.erase(it);
}// ✅ erase returns the next valid iterator
for (auto it = v.begin(); it != v.end(); ) {
if (*it < 0) it = v.erase(it);
else ++it;
}
// ✅✅ even better — erase-remove idiom
v.erase(remove_if(v.begin(), v.end(),
[](int x){ return x < 0; }), v.end());
Sometimes you want your own type to be range-iterable:
class Range {
int lo, hi;
public:
Range(int l, int h) : lo(l), hi(h) {} struct Iterator {
int v;
int operator*() const { return v; }
Iterator& operator++() { ++v; return *this; }
bool operator!=(const Iterator& o) const { return v != o.v; }
};
Iterator begin() const { return {lo}; }
Iterator end() const { return {hi}; }
};
for (int x : Range(1, 6)) cout << x; // 12345
In modern code, prefer std::views::iota(1, 6) — that's exactly what it does.
for (auto& x : v) { /* ... */ }// Compiles roughly to:
{
auto __end = v.end();
for (auto __it = v.begin(); __it != __end; ++__it) {
auto& x = *__it;
/* ... */
}
}
Works on anything with begin() / end() — your custom types, arrays, ranges, etc.
| Need | Code | |
| Iterate forward | for (auto& x : v) | |
| Iterate reverse | for (auto& x : v \ | views::reverse) |
| Index from iterator | distance(v.begin(), it) | |
| Move iterator | advance(it, 3) | |
| Last element | prev(v.end()) or v.back() | |
| Append in algorithm | back_inserter(v) | |
| Read from cin | istream_iterator | |
| Write to cout | ostream_iterator | |
| Erase while looping | it = v.erase(it); |
You now understand the contract every algorithm depends on. Next: modern C++ features that make all of this even nicer.