Итераторы
Цикл по диапазону обходит все элементы контейнера, но не сообщает, где лежит текущий. А функциям, которые удаляют элементы, нужно именно место элемента в контейнере. У std::map и std::set индексов нет, поэтому место обозначает итератор (iterator) — объект, который указывает на элемент контейнера:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{10, 20, 30};
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << ' '; // 10 20 30
}
}begin и end
v.begin() возвращает итератор на первый элемент, а v.end() — на место сразу за последним. Элемента в позиции end() нет, и чтение *v.end() — неопределённое поведение. Поэтому цикл идёт, пока it != v.end(). У пустого контейнера begin() равен end(), и тело цикла не выполняется ни разу.
Итератор похож на указатель (pointer): *it даёт элемент, а ++it переходит к следующему. Полное имя его типа — std::vector<int>::iterator, поэтому тип выводят через auto. Так же устроены итераторы std::map и std::set, а цикл по диапазону — сокращённая запись такого цикла.
Удаление по итератору
v.erase(it) удаляет элемент, на который указывает it, и сдвигает следующие элементы на его место. После этого it и все итераторы за ним становятся недействительными (invalidated): пользоваться ими — неопределённое поведение. Взамен erase возвращает итератор на элемент, который шёл за удалённым. Поэтому цикл удаления продвигает итератор сам:
std::vector<int> v{1, 2, 4, 5, 6};
for (auto it = v.begin(); it != v.end();) {
if (*it % 2 == 0) {
it = v.erase(it);
} else {
++it;
}
}
for (int x : v) {
std::cout << x << ' '; // 1 5
}Шага в заголовке цикла нет. После удаления it уже указывает на следующий элемент, а ++it выполняется, только если ничего не удалили. У std::map и std::set вызов erase(it) делает недействительным только итератор на удалённый элемент, а цикл удаления пишут так же.
Частая ошибка
Цикл по вектору добавляет элементы в этот же вектор:
std::vector<int> v{1, 2, 3};
for (int x : v) {
v.push_back(x * 10); // ✗
}Цикл по диапазону получает итераторы begin() и end() один раз, до первой итерации. push_back всегда делает недействительным end(), а при перевыделении памяти — и все итераторы, указатели и ссылки на элементы. Поэтому этот цикл — неопределённое поведение при любой ёмкости. Запомните размер до цикла и обходите вектор по индексу: индекс остаётся верным и после перевыделения памяти.
std::vector<int> v{1, 2, 3};
std::size_t n = v.size();
for (std::size_t i = 0; i < n; ++i) {
v.push_back(v[i] * 10);
}
for (int x : v) {
std::cout << x << ' '; // 1 2 3 10 20 30
}