Learn C++ STL

Lesson 1 of 9 ¡ Iterators: The Thing Between a Container and an Algorithm

Module 11 ¡ Iterators: The Thing Between a Container and an Algorithm

Iterators: A Position That Knows How to Move

FreeReading

In this lesson

  • Name the category of every container's iterator, input, output, forward, bidirectional or random access, and say what each category can do.
  • Walk any container forward from begin() to the past-the-end end(), backward from rbegin() to rend(), and read-only with cbegin().
  • Explain in one sentence what an invalidated iterator is, and say when a vector's iterators break.

Kenji reads the water level of the school's rain tank every hour and keeps the readings in a vector<int>. His function printGaps(v) prints how much the level changed each hour, with v[i] - v[i - 1]. Maria wants the same report for her own tank. Her log takes late readings in the middle, so she keeps it in a std::list, and her very first l[1] stops GCC 12. Does every container need its own copy of the function?

One function for a vector and a list

Maria's first try is Kenji's index read on her list<int> l: cout << l[1] - l[0];. It stops with an error at the runner's flags, once for each index: error: no match for 'operator[]' (operand types are 'std::__cxx11::list<int>' and 'int'). A list has no [], because it cannot jump to position 1 without walking there. What both containers do have is an iterator: a position that can read the element there and move on to the next one.

So Kenji rewrites printGaps to take two iterators, first and last, instead of a vector. The line template <typename It> lets It be any iterator type. The compiler then writes one copy for the vector's iterators and one for the list's. Module 1 lesson 05 showed the idea, and this track writes only this small form of it.

#include <iostream>
#include <iterator>
#include <list>
#include <vector>
using namespace std;

template <typename It>
void printGaps(It first, It last) {
    cout << "gaps:";
    if (first != last) {
        It before = first;
        ++first;
        while (first != last) {
            cout << ' ' << *first - *before;
            before = first;
            ++first;
        }
    }
    cout << '\n';
}

int main() {
    vector<int> kenji{12, 15, 11, 18, 18};
    list<int> maria{12, 15, 11, 18, 18};
    maria.insert(next(maria.begin(), 2), 20);
    printGaps(kenji.begin(), kenji.end());
    printGaps(maria.begin(), maria.end());
    return 0;
}
gaps: 3 -4 7 0
gaps: 3 5 -9 7 0

Inside, the function only compares two iterators with !=, reads with * and steps with ++. Both containers offer those three moves, so the same text compiled for both. Maria's list took 20 in the middle, and her gaps changed with it. So a function written over two iterators serves every container that can walk.

begin() and end() on three shapes of memory

Module 0 lesson 04 met the walk. begin() is the first element, end() is the empty slot one past the last, and the loop runs while it != end(). Together they make the half-open range [begin, end), which any algorithm takes from any container. What changes from container to container is the memory under the walk. The widget plays one loop over the same four values in three containers, five stops each.

Fifteen stops: the same loop walks 12, 15, 11 and 18 in a vector, a deque and a list. Each row gives the walk, where it is, and what *it reads.

WalkWhere it isWhat *it reads
std::vector<int> cbegin(), position 0 of the one block12
std::vector<int> cposition 1, the next address15
std::vector<int> cposition 2, the next address11
std::vector<int> cposition 3, the next address18
std::vector<int> cend(), the slot past 18end(), not read
std::deque<int> cbegin(), block 1, slot 112
std::deque<int> cblock 1, slot 215
std::deque<int> cblock 2, slot 1: a hop to the next block11
std::deque<int> cblock 2, slot 218
std::deque<int> cend(), the slot past 18end(), not read
std::list<int> cbegin(), node 112
std::list<int> cnode 2, reached by a link15
std::list<int> cnode 3, reached by a link11
std::list<int> cnode 4, reached by a link18
std::list<int> cend(), past the last nodeend(), not read

The vector keeps its four values side by side in one block, so ++ moves to the next address. The deque keeps fixed-size blocks. The drawing uses blocks of 2, but GCC 12's real block holds 128 ints, so ++ hops to a new block only at a block's edge. Every list node is a separate allocation, wherever free memory happened to be, so ++ follows a link. A program on Compiler Explorer printed the same walk, 12 15 11 18 and then end(), for all three.

So the loop reads the same everywhere, but one ++ is a different move in each container. That difference decides which moves an iterator offers.

Five categories: what an iterator can do

The C++ standard sorts iterators into five categories by the moves they offer. Each container's iterator belongs to one of them. GCC 12 reports it through std::iterator_traits, a lookup that lesson 02 runs. This is the table to keep.

CategoryRead *itWrite *it = x++--+ n, - n, [n], <, it2 - it1Who gives it on GCC 12
inputyes, one passnoyesnonoistream_iterator
outputnoyes, one passyesnonoostream_iterator, back_insert_iterator
forwardyes, many passesyes, except keysyesnonoforward_list, unordered_set, unordered_map
bidirectionalyesyes, except keysyesyesnolist, set, map
random accessyesyesyesyesyesvector, deque, array, string, a plain pointer such as int*

"Except keys" means a set's elements and a map's keys stay read-only, as Modules 8 and 10 showed; a map's value can still be written. One pass means each value is gone once the iterator moves on: a number read from the keyboard cannot be read again. Read the table from the bottom up. A random access iterator does everything, and each row above it gives something up.

The five iterator categories as nested boxes Each box can do everything the boxes inside it can random access: + n, - n, [n], <, it2 - it1 vector, deque, array, string, int* bidirectional: adds -- list, set, map forward: many passes, *it = x forward_list, unordered_set, unordered_map input: read *it, ++ istream_iterator each value is read once, then the iterator moves on a kind of its own output: *it = x, ++ ostream_iterator back_insert_iterator writes, never reads
The categories GCC 12 reports for each container. An iterator can be used wherever any box inside its own is asked for.

The boxes nest because each category can do everything the one inside it can. So std::find, which needs only an input iterator, takes any container. std::sort asks for random access, so a list cannot use it and sorts itself instead, as Module 5 showed. A plain pointer is random access too, which is why a C array works with every algorithm.

In C++20

C++20 names each category as a concept, a yes-or-no test on a type that the compiler answers. It also adds a sixth, contiguous_iterator, for elements that sit side by side in memory. The program asks five questions and prints 1 for yes, 0 for no. This block runs at C++20, and its Run button opens it there.

#include <forward_list>
#include <iostream>
#include <iterator>
#include <list>
#include <vector>
using namespace std;

int main() {
    cout << contiguous_iterator<vector<int>::iterator> << ' '
         << random_access_iterator<vector<int>::iterator> << ' '
         << random_access_iterator<list<int>::iterator> << ' '
         << bidirectional_iterator<list<int>::iterator> << ' '
         << bidirectional_iterator<forward_list<int>::iterator> << '\n';
    return 0;
}
1 1 0 1 0
Run in Compiler

The four ends: begin, end, rbegin and rend

A container that can walk backwards gives two more ends. rbegin() is a reverse iterator on the last element, and rend() is the empty slot before the first. A reverse iterator turns ++ around, so each ++ moves one element towards the front. The backward loop is the forward loop with an r in front of two names.

begin, end, rbegin and rend on one vector Four ends on one vector 12 15 11 18 begin() end() rbegin() rend() Forward: begin() to end(). Backward: rbegin() to rend(), each ++ one box to the left. The dashed slots hold no element, so neither end() nor rend() is ever read.
begin() and rbegin() sit on real elements; end() and rend() are the empty slots where each walk stops.
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{12, 15, 11, 18};
    cout << "*begin() " << *v.begin() << ", *rbegin() " << *v.rbegin() << '\n';
    cout << "forward: ";
    for (auto it = v.begin(); it != v.end(); ++it) {
        cout << ' ' << *it;
    }
    cout << "\nbackward:";
    for (auto it = v.rbegin(); it != v.rend(); ++it) {
        cout << ' ' << *it;
    }
    cout << '\n';
    cout << "end() - begin() = " << v.end() - v.begin() << ", rend() - rbegin() = " << v.rend() - v.rbegin() << '\n';
    return 0;
}
*begin() 12, *rbegin() 18
forward:  12 15 11 18
backward: 18 11 15 12
end() - begin() = 4, rend() - rbegin() = 4

*rbegin() read 18, the last value, and the backward walk stopped at rend() without reading it. Both ranges hold four elements: rend() - rbegin() is 4, like end() - begin(). Every container with a bidirectional or random access iterator has rbegin(). A forward_list has none: GCC 12 says error: 'class std::forward_list<int>' has no member named 'rbegin'; did you mean 'begin'?, and an unordered_set gets the same answer. So walk backwards with rbegin() and rend(), and keep != as the test.

cbegin(): a walk that only reads

cbegin() and cend() are begin() and end() for a walk that promises not to write. They return a const_iterator, which reads *it but refuses *it = x. crbegin() and crend() make the same promise backwards. Use them when a loop only looks, and the compiler will hold you to it. Example 3 below shows the refusal in GCC 12's words.

What a list iterator refuses

A list iterator is bidirectional: it steps with ++ and --, one link at a time. It has no + n and no <, because neither is a single step along a link. GCC 12 refuses l.begin() + 2 and it < l.end(), and mistakes 2 and 3 quote its messages. Move k steps with std::next(it, k) instead, and end every loop with !=.

#include <iostream>
#include <iterator>
#include <list>
using namespace std;

int main() {
    list<int> l{12, 15, 11, 18};
    auto third = next(l.begin(), 2);
    cout << "third: " << *third << '\n';
    --third;
    cout << "one back: " << *third << '\n';
    int steps = 0;
    for (auto it = l.begin(); it != l.end(); ++it) {
        steps++;
    }
    cout << "steps to end(): " << steps << '\n';
    return 0;
}
third: 11
one back: 15
steps to end(): 4

next(l.begin(), 2) followed two links to 11, and --third stepped back to 15. The loop with != took four steps to reach end(). On a vector, next jumps in one move, while on a list it walks; lesson 02 measures the difference. So write next and !=, and the same code runs on every container.

When an iterator stops being valid

An iterator is only a position. If the container moves its elements, a saved iterator still points at the old place, and it is invalidated. Reading through it is undefined behaviour: the C++ standard promises nothing about what happens. A vector moves everything when it outgrows its block, as Module 2 showed. This program fills a vector up to its capacity of 4, then adds a fifth value.

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;
    v.reserve(4);
    for (int x : {10, 20, 30, 40}) v.push_back(x);
    const int* before = v.data();
    cout << "size " << v.size() << ", capacity " << v.capacity() << '\n';
    v.push_back(50);
    cout << "size " << v.size() << ", capacity " << v.capacity() << '\n';
    cout << "first element moved: " << (v.data() != before) << '\n';
    return 0;
}
size 4, capacity 4
size 5, capacity 8
first element moved: 1
An iterator saved before push_back(50) points into the freed block push_back(50) on a full vector moves every element Before: capacity 4, and the block is full 10 20 30 40 old block: freed after the move it, saved before 4 values moved After: a new block, capacity 8 10 20 30 40 50 v.begin(), fresh it still points into the freed block: reading *it is undefined behaviour.
The old block is freed after the move. The iterator saved before the push still points into it.

The fifth value did not fit, so the vector took a new block of 8, moved all four values over and freed the old block. An iterator saved before push_back(50) still points into that freed block. Nothing warns you, and the program may even print the old value by luck. So after anything that may grow a vector, take fresh iterators from begin(). Lesson 02 gives the rules for every container, and lesson 05 replays the move step by step.

The syntax you need on day one

Iterators: the ends, the moves, and a function over a range

c.begin()    c.end()          the first element, and the empty slot past the last
c.rbegin()   c.rend()         the last element, and the empty slot before the first
c.cbegin()   c.cend()         begin and end for a walk that only reads
*it                           the element it points at (never *c.end())
++it                          one step forward, on every container
--it                          one step back (not forward_list, not unordered)
it != c.end()                 the loop test that works on every container
next(it, k)   prev(it, k)     a copy of it moved k steps forward or back

template <typename It>
void f(It first, It last)     one function for any container's range
  • Every container has begin() and end(); [begin(), end()) is the whole container, and an empty one has begin() == end().
  • auto it = c.begin(); lets the compiler name the iterator's type, which is long: vector<int>::iterator, list<string>::const_iterator.
  • next and prev live in <iterator>. They jump on random access and walk one step at a time elsewhere.
  • Arithmetic, it + 2, it2 - it1 and it < end, exists on random access iterators only.

So one loop shape, begin() to end() with != and ++, walks every container on this track.

Example 1: one printAll for a vector and a list

The smallest function over two iterators prints a range in brackets. It runs on a vector of marks, a list of snacks, part of that list, and an empty range.

#include <iostream>
#include <iterator>
#include <list>
#include <string>
#include <vector>
using namespace std;

template <typename It>
void printAll(It first, It last) {
    cout << '[';
    for (It it = first; it != last; ++it) {
        if (it != first) {
            cout << ", ";
        }
        cout << *it;
    }
    cout << "]\n";
}

int main() {
    vector<int> marks{4, 8, 15};
    list<string> snacks{"tea", "bun", "jam"};
    printAll(marks.begin(), marks.end());
    printAll(snacks.begin(), snacks.end());
    printAll(next(snacks.begin()), snacks.end());
    printAll(marks.end(), marks.end());
    return 0;
}
[4, 8, 15]
[tea, bun, jam]
[bun, jam]
[]

The same function printed an int vector and a string list. next(snacks.begin()) started the range one element in, so tea was left out. printAll(marks.end(), marks.end()) was an empty range, so the loop never ran.

Run in Compiler
Example 2: browser history, newest first

Amara's browser keeps every page she opened in a list<string>, oldest first. The history page shows them newest first, so it walks from rbegin() to rend(). The Back button goes to the page one step back from the newest.

#include <iostream>
#include <iterator>
#include <list>
#include <string>
using namespace std;

int main() {
    list<string> history{"home", "news", "maps", "mail"};
    history.push_back("music");
    cout << "newest first:\n";
    int n = 1;
    for (auto it = history.rbegin(); it != history.rend(); ++it) {
        cout << n << ". " << *it << '\n';
        n++;
    }
    cout << "now on " << *history.rbegin() << ", Back goes to " << *next(history.rbegin()) << '\n';
    return 0;
}
newest first:
1. music
2. mail
3. maps
4. news
5. home
now on music, Back goes to mail

The walk printed music first, the page added last, and home, the first page, at the end. next(history.rbegin()) moved one step towards the front, onto mail. A list is bidirectional, so the reverse walk costs one link per step, the same as the forward one.

Run in Compiler
Example 3: a read-only walk with cbegin

Zara adds up the prices of a shopping list and counts the items over 100. The loop only reads, so it walks with cbegin() and cend().

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> price{120, 45, 80, 300, 60};
    int total = 0;
    int over100 = 0;
    for (auto it = price.cbegin(); it != price.cend(); ++it) {
        total += *it;
        if (*it > 100) {
            over100++;
        }
    }
    cout << "total " << total << ", over 100: " << over100 << '\n';
    cout << "first price still " << price[0] << '\n';
    return 0;
}
total 605, over 100: 2
first price still 120

The total is 605, and two prices are over 100. Now Bob wants to cap every price at 100 and writes *it = 100; inside the same cbegin() loop. GCC 12 stops at the runner's flags: error: assignment of read-only location 'it.__gnu_cxx::__normal_iterator<const int*, std::vector<int> >::operator*()'. The const int* inside the name is the promise: this iterator only reads. To change the prices, walk with begin() instead.

Run in Compiler

Where this is used

  • LLVM. A BasicBlock, a straight run of a compiler's instructions, offers begin() and end() over its instruction list. The LLVM Programmer's Manual walks it with for (Instruction &I : BB), a range-for over those iterators.
  • Python's for statement. for x in items first calls iter(items) to get an iterator, then calls next() on it for each element. When nothing is left, the iterator raises StopIteration, which plays the part of end().
  • Java's Iterator. A Java collection hands out an Iterator with hasNext() and next(). The Java Language Specification turns an enhanced for over a collection into exactly those two calls.

Common mistakes

1. Indexing a list.

list<int> l{12, 15, 11, 18};
cout << l[1] - l[0] << '\n';

An error at the runner's flags, once per index: error: no match for 'operator[]' (operand types are 'std::__cxx11::list<int>' and 'int'). A list cannot reach position 1 without walking to it, so it offers no []. Walk it with an iterator, or write *next(l.begin(), 1) for one element. You will write it because v[i] is the first loop everyone learns.

2. Adding a number to a list iterator.

list<int> l{12, 15, 11, 18};
auto it = l.begin() + 2;

An error at the runner's flags: error: no match for 'operator+' (operand types are 'std::__cxx11::list<int>::iterator' and 'int'). A page of note: candidate: lines follows; the first line is the one that matters. Write auto it = next(l.begin(), 2);. You will write it because the same line works on a vector.

3. Ending a list loop with <.

for (auto it = l.begin(); it < l.end(); ++it) {
    cout << *it << ' ';
}

An error at the runner's flags: error: no match for 'operator<' (operand types are 'std::_List_iterator<int>' and 'std::__cxx11::list<int>::iterator'), then a page of candidates. Two list nodes have no order in memory, so there is nothing for < to compare. Write it != l.end(), which works on every container. You will write it because counting loops say i < n.

Brain teaser

Zara prints two facts about the reverse walk of a vector.

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{10, 20, 30, 40};
    cout << *v.rbegin() << ' ' << (v.rbegin().base() == v.end()) << '\n';
    return 0;
}

GCC 12 prints 40 1. base() gives the plain iterator a reverse iterator keeps inside it. So v.rbegin() keeps v.end(), the empty slot after 40, yet *v.rbegin() reads 40. How can both be true, and why would the library build it that way? Answer in two or three sentences.

If a reverse iterator kept the position of its own element, where would rend() have to sit? Ask whether a container has any position before its first element.

Exercise 1Easy

Maria's street runs from west to east and ends at the sea. From the top floor of a building you can see the water when it is strictly taller than every building to its east. The last building, right by the sea, always has the view. Maria wants to know which buildings have it. The starter reads every height until the input ends; lesson 02 explains its first line.

Input. The heights of the buildings in metres, from west to east, separated by spaces or line breaks, until the input ends.

Output. On the first line, how many buildings have a sea view. On the second line, their positions from west to east, counted from 1, separated by single spaces.

Constraints. 1 to 100000 buildings, each from 1 to 1000000 metres tall.

Sample. Input 5 9 4 7 2 6 3 gives 4 and 2 4 6 7. The 3 by the sea has the view, and 6, 7 and 9 are each taller than everything east of them.

#include <deque>
#include <iostream>
#include <iterator>
#include <vector>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // Every height until the input ends: lesson 02 explains this line.
    vector<int> height(istream_iterator<int>(cin), {});

    deque<int> view;   // the positions with a sea view, west to east
    // Walk from the sea end with a reverse iterator and keep the tallest
    // height seen so far. A building taller than all of them has the view:
    // put its position, counted from 1, at the front of view.

    cout << view.size() << '\n';
    // Print the positions in view, separated by single spaces.
    return 0;
}

Graded as sea-view, a free problem in this module's problem set. The hidden tests include a single building and ten buildings of one height. They also include 100000 heights that only rise, 100000 that only fall, and 1000 buildings of only ten different heights.

Run in Compiler
Exercise 2Medium

Zara writes one letter for each day's weather: S for sunny, R for rainy and C for cloudy. She keeps the log in a std::list<char>, because she often adds a forgotten day in the middle. Now she wants it shortened: every run of equal letters becomes the letter and the length of the run.

Input. One line of letters, each S, R or C.

Output. One line: each run as its letter and its length, the runs separated by single spaces.

Constraints. The line holds 1 to 100000 letters.

Sample. Input SSSRRCS gives S3 R2 C1 S1. The last S is a run of its own, because a cloudy day came between.

#include <iostream>
#include <iterator>
#include <list>
#include <string>
using namespace std;

int main() {
    string line;
    cin >> line;
    list<char> days(line.begin(), line.end());   // one letter per day

    // Walk days with an iterator. For every run of equal letters, print the
    // letter and the length of the run, the runs separated by single spaces.

    return 0;
}

Not graded on its own. Keep one iterator on the first day of a run, and step a second one with ++ until it meets another letter or end().

Run in Compiler
Exercise 3Hard

Kenji's tank sensor sometimes jumps. Wherever two neighbouring readings in his vector<int> differ by more than d, he inserts their average between them, the sum divided by 2 and rounded down. A value he inserted is never compared again, so each gap gets at most one new reading.

Input. A line with n and d, then the n readings.

Output. One line: the readings after every insert, separated by single spaces.

Constraints. 2 <= n <= 1000, 0 <= d <= 1000000, and every reading is from 0 to 1000000.

Sample. Input 5 5 and 10 12 30 31 50 gives 10 12 21 30 31 40 50. 12 and 30 differ by 18, so 21 goes between them, and 31 and 50 get 40.

#include <cstdlib>
#include <iostream>
#include <iterator>
#include <vector>
using namespace std;

int main() {
    int n, d;
    cin >> n >> d;
    vector<int> readings(n);
    for (int& x : readings) {
        cin >> x;
    }

    // Walk readings with an iterator. Between two neighbours that differ by
    // more than d, insert their average (their sum divided by 2, rounded
    // down). A value you inserted is never compared again. Then print
    // readings on one line, separated by single spaces.

    return 0;
}

Not graded on its own. The idea the lesson only pointed at: insert may move the whole vector, which invalidates every saved iterator. Keep the iterator that insert returns, which points at the new value, and step on from there.

Run in Compiler

Common doubts

  • Is an iterator just a pointer?

    On a vector, GCC 12's iterator is a pointer wrapped in a small class, 8 bytes. A list's iterator is also 8 bytes, but it holds a pointer to a node, and its ++ follows the node's link. Lesson 05 opens both.

  • Why do loops test it != end() and not it < end()?

    Because < exists only on random access iterators, and != exists on all of them. A loop with != runs unchanged on a vector, a list or a set.

  • Can I read *end() to get the last element?

    No. end() is the empty slot after the last element, and reading it is undefined behaviour. The last element is *rbegin(), or *prev(end()), and only when the container is not empty.

  • When should I write an iterator loop instead of a range-for?

    A range-for is an iterator loop the compiler writes for you, from begin() to end(). Write the loop yourself when you need the position, a backward walk, or two containers at once. Lesson 04 sorts the cases.

Key takeaways

  • An iterator is a position that reads its element with * and moves with ++; begin() is the first element and end() the empty slot after the last.
  • The five categories, input, output, forward, bidirectional and random access, say which moves an iterator has; only random access has + n and <.
  • vector, deque, array, string and plain pointers give random access; list, set and map bidirectional; forward_list and the unordered containers forward.
  • rbegin() to rend() walks backwards, cbegin() to cend() walks without writing, and != with next works on every container.
  • A vector that outgrows its capacity moves every element, and every iterator saved before the move is invalidated.
  • Go deeper: Under the Hood, an iterator is a wrapped pointer (Pro).

Next, lesson 02 takes every iterator operation one by one, with what it costs on each category.

End of lesson 1

Mark it done, and your progress moves with you.

Next: Every Iterator Operation, One by One, With Its Cost