14.7. Using iterators¶
An iterator is an object that identifies a position in a container.
It can be dereferenced like a pointer to access the element at that position.
When a range-based for loop is not the right tool, an explicit iterator
allows a program to control traversal more precisely.
14.7.1. Declaring and dereferencing an iterator¶
The iterator type is associated with the container and its element type.
For example, std::vector<int>::iterator and
std::vector<std::string>::iterator are different types.
In modern C++, auto is usually the clearest way to declare an iterator:
1#include <iostream>
2#include <vector>
3
4int main() {
5 std::vector<int> nums = {1, 2, 3, 4, 5};
6 auto it = nums.begin();
7
8 std::cout << *it << '\n';
9}
The expression *it accesses the element. The iterator itself represents
a position; it is not the element value. The iterator must be dereferenceable,
so nums.end() cannot be dereferenced.
14.7.2. Traditional iterator loops¶
An explicit iterator can be used in a traditional for loop:
1#include <iostream>
2#include <vector>
3
4int main() {
5 const std::vector<int> nums = {1, 2, 3, 4, 5};
6 std::cout << "nums contains:";
7
8 for (auto it = nums.begin(); it != nums.end(); ++it) {
9 std::cout << ' ' << *it;
10 }
11 std::cout << '\n';
12}
The same traversal can be written with a while loop. Braces are required
when the loop body contains both the output operation and the increment:
1#include <iostream>
2#include <vector>
3
4int main() {
5 const std::vector<int> nums = {1, 2, 3, 4, 5};
6 std::cout << "nums contains:";
7
8 auto it = nums.begin();
9 while (it != nums.end()) {
10 std::cout << ' ' << *it;
11 ++it;
12 }
13 std::cout << '\n';
14}
When the loop body does not need to modify the container, a range-based
for loop is usually shorter and clearer:
1#include <iostream>
2#include <vector>
3
4int main() {
5 const std::vector<int> nums = {1, 2, 3, 4, 5};
6 std::cout << "nums contains:";
7
8 for (const auto& num : nums) {
9 std::cout << ' ' << num;
10 }
11 std::cout << '\n';
12}
The range declaration initializes a loop variable from each element.
With const auto&, it binds a read-only reference and avoids copying the
element. Use auto& when the loop should modify elements, or auto when
an independent copy is useful. Unlike *it, the range variable is already
the element value or reference; it is not an iterator and must not be
dereferenced.
14.7.3. Limits of range-based for¶
Range-based for is designed to visit every element in order.
It is not the best fit when a loop needs a custom stopping condition or must
coordinate traversal through multiple containers. An explicit loop can stop
where the program needs:
1#include <iostream>
2
3int main() {
4 for (int value = 32; value > 0; value /= 2) {
5 std::cout << value << ' ';
6 }
7 std::cout << '\n';
8}
When two containers must be traversed together, keep one iterator for each container and stop when either reaches its end:
1#include <iostream>
2#include <list>
3#include <vector>
4
5int main() {
6 const std::vector<int> left = {1, 2, 3, 4};
7 const std::list<int> right = {10, 20, 30};
8
9 auto left_it = left.begin();
10 auto right_it = right.begin();
11 while (left_it != left.end() && right_it != right.end()) {
12 std::cout << *left_it + *right_it << ' ';
13 ++left_it;
14 ++right_it;
15 }
16 std::cout << '\n';
17}
C++20 also provides ranges and views for composing some partial traversals. Explicit iterators remain useful when teaching iterator mechanics or when a loop needs stateful control that is clearer as an imperative loop.
14.7.4. Container functions that require iterators¶
Container functions that use position information generally accept iterators
rather than an integral position or an index like operator[].
insertInserts elements before the position identified by an iterator.
The following example inserts one value, repeated values, a range from another
container, and a raw array range. The iterator returned by insert points
to the first inserted element. Vector insertion can invalidate existing
iterators, so the example obtains a fresh iterator before each later operation.
1#include <iostream>
2#include <iterator>
3#include <vector>
4
5void print(const std::vector<int>& values) {
6 for (const auto value : values) {
7 std::cout << ' ' << value;
8 }
9 std::cout << '\n';
10}
11
12int main() {
13 std::vector<int> nums(3, 100);
14 print(nums);
15
16 auto it = nums.insert(nums.begin(), 200);
17 print(nums);
18
19 it = nums.insert(nums.begin(), 2, 300);
20 print(nums);
21
22 std::vector<int> fib = {1, 1, 2, 3, 5, 8, 13, 21};
23 it = nums.begin() + 2;
24 nums.insert(it, fib.begin(), fib.end());
25 print(nums);
26
27 int values[] = {501, 502, 503};
28 nums.insert(nums.begin(), std::begin(values), std::end(values));
29 print(nums);
30}
eraseRemoves one element or a contiguous range of elements.
The following example erases the first element, erases a range, and then
removes every even value. vector::erase returns an iterator to the element
that follows the erased range. Erasing from a vector invalidates iterators and
references at or after the erase position, so the returned iterator must be
used for continued traversal.
1#include <iostream>
2#include <vector>
3
4void print(const std::vector<int>& values) {
5 for (const auto value : values) {
6 std::cout << ' ' << value;
7 }
8 std::cout << '\n';
9}
10
11int main() {
12 std::vector<int> nums = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
13 print(nums);
14
15 nums.erase(nums.begin());
16 print(nums);
17
18 nums.erase(nums.begin() + 2, nums.begin() + 5);
19 print(nums);
20
21 nums = {2, 7, 1, 8, 2, 8, 1, 8, 2, 8, 4, 5, 9};
22 for (auto it = nums.begin(); it != nums.end();) {
23 if (*it % 2 == 0) {
24 it = nums.erase(it);
25 } else {
26 ++it;
27 }
28 }
29 print(nums);
30}
C++20 Feature
std::erase_if(nums, predicate) is a concise alternative
when the goal is simply to remove elements matching a condition. The
following example removes all odd values from the first ten Fibonacci
numbers:
1#include <algorithm>
2#include <iostream>
3#include <vector>
4
5void print(const std::vector<int>& values) {
6 for (const auto value : values) {
7 std::cout << ' ' << value;
8 }
9 std::cout << '\n';
10}
11
12int main() {
13 std::vector<int> fibonacci = {0, 1, 1, 2, 3, 5, 8, 13, 21, 34};
14 print(fibonacci);
15
16 std::erase_if(fibonacci, [](int value) {
17 return value % 2 != 0;
18 });
19 print(fibonacci);
20}
The explicit loop above shows why an iterator must advance differently
after erase than after an element is retained.