Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

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 for is 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() to end(), where end() is one past the last element.
  • *it dereferences, it->m accesses a member, ++it advances.
  • 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 use std::erase_if.

Questions about this lesson

Log in to ask a question.