Lesson 6 of 9
There are three ways to walk a container. They aren't interchangeable, and knowing which to reach for is a real skill.
1. Index loop
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> names = {"ada", "grace", "alan"};
for (std::size_t i = 0; i < names.size(); ++i) {
std::cout << i << ": " << names[i] << "\n";
}
return 0;
}
0: ada
1: grace
2: alan
Use std::size_t for the counter, matching what .size() returns — mixing signed and unsigned is
the lesson 3.3 trap, and -Wall will warn.
Use it when you need the position, or you're stepping by something other than 1, or you're
comparing v[i] with v[i+1].
2. Range-based for
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> names = {"ada", "grace", "alan"};
for (const std::string& name : names) {
std::cout << name << " ";
}
std::cout << "\n";
std::vector<int> values = {1, 2, 3};
for (int& v : values) {
v *= 10;
}
for (int v : values) {
std::cout << v << " ";
}
std::cout << "\n";
return 0;
}
ada grace alan
10 20 30
Lesson 4.4 covered this. The & rules still apply: const T& to read without copying, T& to
modify, plain T for cheap types you only read.
Use it when you just need each element. This is the default — it can't run off the end and there's no index to get wrong.
3. Iterators
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> names = {"ada", "grace", "alan"};
for (std::vector<std::string>::iterator it = names.begin(); it != names.end(); ++it) {
std::cout << *it << " ";
}
std::cout << "\n";
for (auto it = names.begin(); it != names.end(); ++it) {
std::cout << it->size() << " ";
}
std::cout << "\n";
return 0;
}
ada grace alan
3 5 4
An iterator is an object that points at an element and knows how to move to the next one.
begin()— the first element.end()— one past the last. Not an element; a marker.*it— the element it points at.it->member— shorthand for(*it).member.++it— advance.
The first loop spells the type out; the second uses auto (lesson 3.6), which is what everyone
actually writes.
end() being one-past-the-end is why the condition is it != end(), not <=. The same
half-open convention as i < size().
Use them when you need to modify the container while iterating, work with a container that
has no index (std::map, std::list), or call a standard algorithm.
Why iterators exist
The range-based for you've been using is iterators underneath — the compiler rewrites it into a
begin()/end() loop. And they're what lets one algorithm work on every container:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {5, 2, 9, 1, 7};
std::sort(v.begin(), v.end());
for (int x : v) {
std::cout << x << " ";
}
std::cout << "\n";
auto found = std::find(v.begin(), v.end(), 9);
if (found != v.end()) {
std::cout << "found 9 at index " << (found - v.begin()) << "\n";
}
std::cout << std::count_if(v.begin(), v.end(), [](int x) { return x > 4; }) << "\n";
return 0;
}
1 2 5 7 9
found 9 at index 4
3
std::sort doesn't know what a vector is. It takes two iterators and works on anything that
provides them. That's the abstraction Module 13 is built on — and the [](int x) { ... } is a
lambda, which is Module 14.
Note std::find returns end() when there's no match, so always compare against end()
before dereferencing.
Erasing while iterating
The one case that genuinely needs iterators:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6};
for (auto it = v.begin(); it != v.end(); ) {
if (*it % 2 == 0) {
it = v.erase(it); // erase returns the next valid iterator
} else {
++it;
}
}
for (int x : v) {
std::cout << x << " ";
}
std::cout << "\n";
return 0;
}
1 3 5
Two things to notice. There's no ++it in the loop header — the body advances conditionally,
because erase already moved us on. And erase invalidates the iterator you gave it, which
is why you must use its return value rather than the old one.
Getting this wrong is a classic use-after-free. In real code, prefer the erase-remove idiom or
C++20's std::erase_if:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6};
std::erase_if(v, [](int x) { return x % 2 == 0; });
for (int x : v) {
std::cout << x << " ";
}
std::cout << "\n";
return 0;
}
1 3 5
One line, no iterator handling, no chance of getting it wrong.
Choosing
| Loop | Use when |
| --- | --- |
| Range-based for | you just need each element — the default |
| Index | you need the position, a step, or neighbouring elements |
| Iterator | erasing while iterating, non-indexed containers, algorithms |
Try it
Given std::vector<int> v = {4, 8, 15, 16, 23, 42}; print each element with its index, then
find the first element greater than 20 and report its position — using the tool best suited to
each job.
Checkpoint
- Range-based
foris the default: no index, can't overrun. - Index loops when you need the position, a custom step, or adjacent elements. Use
std::size_t. - Iterators are
begin()toend(), whereend()is one past the last element. *itdereferences,it->maccesses a member,++itadvances.- Iterators are what let one algorithm serve every container.
- When erasing while iterating, use
erase's return value and don't advance in the header — or just usestd::erase_if.
Questions about this lesson
Log in to ask a question.