JavaRush /Курсы /C++ SELF /iterator vs

iterator vs const_iterator — безопасный доступ

C++ SELF
58 уровень , 3 лекция
Открыта

1. Зачем нужен const_iterator

Если честно, поначалу кажется, что const_iterator придумали для зануд (или для преподавателей, чтобы было чем вас мучить на контрольной). Мол, какая разница: я же сам знаю, где меняю контейнер, а где нет. Проблема в том, что «сам знаю» обычно работает ровно до первого рефакторинга, дедлайна и момента «я просто быстренько поправлю одну строчку». После этого «одна строчка» превращается в «почему список задач сам себя перетасовал?».

Смысл const_iterator очень практический: он превращает «намерение» в проверяемое правило. Если участок кода должен только читать элементы — мы делаем так, чтобы он физически не мог их поменять. И тогда ошибка превращается не в баг на проде, а в скучное сообщение компилятора, которое, как ни странно, иногда делает вашу жизнь лучше.

Интуитивная модель: «пульт с кнопками» и «пульт без кнопок»

Представьте контейнер как телевизор, а итератор — как пульт. Обычный iterator — это пульт со всеми кнопками: можно переключать каналы, можно прибавлять громкость, можно случайно нажать “Mute” и потом 10 минут искать, куда делся звук. const_iterator — это пульт, у которого оставили только кнопки “вперед/назад” и “показать текущий канал”. Смотреть можно, ломать нельзя.

Технически различие проявляется там, где вы разыменовываете итератор. У iterator выражение *it даёт доступ к элементу так, что его можно менять. У const_iterator выражение *it даёт доступ к элементу только для чтения.

Давайте посмотрим на минимальный пример и почувствуем разницу руками.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};

    auto it = v.begin();     // iterator
    *it = 99;

    std::cout << v[0] << '\n';  // 99
}

А теперь тот же смысл, но «режим чтения»:

#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};

    auto cit = v.cbegin();   // const_iterator
    // *cit = 99;             // нельзя: итератор "только чтение"
}

Обратите внимание: это не «запрет ради запрета». Это способ сказать: “вот этот кусок кода — обзорная экскурсия по данным, без права что-либо переставлять”.

2. begin/end и cbegin/cend: включаем режим чтения

Когда вы впервые видите cbegin() и cend(), мозг обычно делает вывод: «ага, это для const-контейнеров». И вот тут начинается тонкость: cbegin()/cend() полезны даже для не-const контейнера, потому что они принудительно дают вам const_iterator — то есть фиксируют намерение: “только читать”. И это очень похоже на то, как const в параметрах функций делает ваш код честнее.

Сценариев два.

Первый: контейнер сам по себе const. Тогда даже begin() вернёт const_iterator.

#include <vector>

int main() {
    const std::vector<int> v{1, 2, 3};

    auto it = v.begin();  // это будет const_iterator
    // *it = 10;           // нельзя: контейнер const
}

Второй: контейнер обычный, но конкретно здесь вы хотите только читать. Тогда используйте cbegin()/cend().

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{1, 2, 3};

    int sum = 0;
    for (auto it = v.cbegin(); it != v.cend(); ++it) {
        sum += *it;
    }

    std::cout << sum << '\n'; // 6
}

И вот это место важно: cbegin() — это не “чтобы компилятор не ругался”, а “чтобы другой человек (или вы через месяц) не ругался”.

Отдельно забавно, что даже стандартная библиотека периодически уточняет нюансы поведения const begin/const_iterator для разных типов, например для std::span в контексте его итераторов. То есть тема не «учебная мелочь», а реальная зона внимания в стандарте.

Таблица: что возвращают begin и cbegin

В какой-то момент полезно перестать «угадывать по наитию» и иметь маленькую карту местности. Ниже — практическая таблица, которая обычно закрывает 80% вопросов “почему оно стало const”.

Выражение Контейнер Что получаем по смыслу Можно менять элемент через *it?
c.begin()
std::vector<int> c;
iterator
Да
c.cbegin()
std::vector<int> c;
const_iterator
Нет
c.begin()
const std::vector<int> c;
const_iterator
Нет
c.cbegin()
const std::vector<int> c;
const_iterator
Нет

Эту таблицу можно читать так: const-контейнер «заражает» всё на чтение, а cbegin() позволяет «заразить на чтение» даже не-const контейнер.

3. Безопасность и алгоритмы

Почему это «безопасный доступ»

Слова “безопасный доступ” иногда звучат как маркетинг, но здесь всё приземлённо. Когда вы обходите контейнер обычным iterator, вы даёте себе возможность сделать любую мелкую глупость: поправить элемент, сбросить флаг, заменить строку, а потом искать баг. const_iterator превращает этот класс багов в ошибку компиляции, то есть в проблему, которая не добежит до пользователя.

Например, вы пишете функцию печати (она должна только читать), но случайно решили «подчистить пробелы» прямо в процессе вывода. Вот так делать не надо:

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> tasks{"learn", "sleep", "repeat"};

    for (auto it = tasks.cbegin(); it != tasks.cend(); ++it) {
        // it->append("!"); // нельзя: const_iterator
        std::cout << *it << '\n';
    }
}

И это отлично: компилятор не позволяет смешать «печать» и «модификацию». У вас сразу появляется естественная архитектура: отдельно читаем, отдельно меняем.

Алгоритмы: какой итератор они возвращают

Алгоритмы возвращают итераторы того же “режима”, в котором вы им дали диапазон.

Если вы вызываете алгоритм с begin()/end(), он работает с изменяемыми итераторами (если контейнер не const) и вернёт iterator. Если вы вызываете с cbegin()/cend(), то алгоритм вернёт const_iterator. То есть вы не сможете случайно поменять найденный элемент.

Посмотрим на простой пример: найдём первое чётное значение (пусть это «приоритет задачи»).

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> prio{3, 5, 8, 9};

    auto it = std::find_if(prio.cbegin(), prio.cend(),
                           [](int x) { return x % 2 == 0; });

    if (it != prio.cend()) {
        std::cout << *it << '\n'; // 8
        // *it = 10;              // нельзя: it это const_iterator
    }
}

Это поведение — часть общей дисциплины константности в стандартной библиотеке, и даже вокруг похожих тем (например, различий cbegin/cend у std::span и свободных std::ranges::cbegin/std::ranges::cend) встречались отдельные обсуждения и уточнения.

4. Range-for и выбор режима доступа

Range-for кажется «просто удобным синтаксисом», но он отлично выражает тот же выбор: читать или менять. Внутри range-for всё равно используются итераторы — просто компилятор пишет этот цикл за вас.

Тут важно, что вы выбираете не “итератор”, а вид переменной цикла: копия, ссылка или const-ссылка. По сути вы выбираете “режим доступа”.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{1, 2, 3};

    for (auto x : v) {        // копия: менять можно, но контейнер не меняется
        x += 10;
    }

    for (auto& x : v) {       // ссылка: меняем контейнер
        x += 10;
    }

    for (const auto& x : v) { // const-ссылка: читаем без копий
        std::cout << x << ' '; // 11 12 13
    }
    std::cout << '\n';
}

Здесь логика простая и практичная: если вам нужно только вывести/посчитать/сравнить — берите const auto&. Если нужно изменить — берите auto&. Если нужен независимый временный объект — берите auto (копию).

5. Практический пример: дисциплина “читаю vs меняю” на списке задач

Сейчас мы соберём маленький кусочек “нашего курса” в виде мини-приложения: список задач. Мы не будем делать ничего сложного — наша цель сегодня не в архитектуре, а в том, чтобы показать: const_iterator — это стиль, который сам себя защищает.

Начнём с модели задачи и контейнера задач:

#include <string>
#include <vector>

struct Task {
    std::string title;
    bool done;
};

int main() {
    std::vector<Task> tasks{
        {"Learn iterators", false},
        {"Drink water", true},
        {"Write code", false}
    };
}

Теперь сделаем функцию печати. Она должна только читать задачи — значит, параметр делаем const std::vector<Task>&, а обход — через cbegin()/cend(). Это как поставить табличку: “руками не трогать”.

#include <iostream>
#include <string>
#include <vector>

struct Task {
    std::string title;
    bool done;
};

void printTasks(const std::vector<Task>& tasks) {
    for (auto it = tasks.cbegin(); it != tasks.cend(); ++it) {
        std::cout << (it->done ? "[x] " : "[ ] ") << it->title << '\n';
        // пример вывода:
        // [ ] Learn iterators
    }
}

А теперь функция, которая меняет задачу: например, отмечает первую незавершённую как выполненную. Здесь нам нужен обычный iterator, потому что мы хотим менять поле done.

#include <string>
#include <vector>

struct Task {
    std::string title;
    bool done;
};

void markFirstUndone(std::vector<Task>& tasks) {
    for (auto it = tasks.begin(); it != tasks.end(); ++it) {
        if (!it->done) {
            it->done = true;
            return;
        }
    }
}

Теперь свяжем это в main. (Да, это ещё не “тонкий main” идеального мира, но мы сейчас учимся на маленьких шагах.)

#include <iostream>
#include <string>
#include <vector>

struct Task {
    std::string title;
    bool done;
};

void printTasks(const std::vector<Task>& tasks) {
    for (auto it = tasks.cbegin(); it != tasks.cend(); ++it) {
        std::cout << (it->done ? "[x] " : "[ ] ") << it->title << '\n';
    }
}

void markFirstUndone(std::vector<Task>& tasks) {
    for (auto it = tasks.begin(); it != tasks.end(); ++it) {
        if (!it->done) {
            it->done = true;
            return;
        }
    }
}

int main() {
    std::vector<Task> tasks{
        {"Learn iterators", false},
        {"Drink water", true},
        {"Write code", false}
    };

    printTasks(tasks);
    // [ ] Learn iterators
    // [x] Drink water
    // [ ] Write code

    markFirstUndone(tasks);

    printTasks(tasks);
    // [x] Learn iterators
    // [x] Drink water
    // [ ] Write code
}

В этом примере важнее всего не “задачи”, а то, что код сам рассказывает историю: printTasks не имеет права портить данные, а markFirstUndone имеет. Вы это видите по сигнатуре и по итераторам. И в этом месте компилятор становится вашим напарником: он не даст «случайно» сделать печать изменяющей.

Если коротко, это выглядит так:

flowchart TD
    A["std::vector<Task> tasks"] --> B["printTasks(const vector&)"]
    B --> C["cbegin/cend -> const_iterator"]
    C --> D["только чтение"]

    A --> E["markFirstUndone(vector&)"]
    E --> F["begin/end -> iterator"]
    F --> G["можно менять элементы"]

6. Типичные ошибки при работе с iterator и const_iterator

Ошибка №1: «Я хотел просто вывести, но почему-то у меня всё стало const и ничего не меняется».
Чаще всего это происходит, когда вы передали контейнер как const std::vector<T>& (или сам контейнер объявлен const). В этом случае begin() возвращает const_iterator, и это нормально: вы сами подписали контракт “не меняю”. Если изменение действительно нужно, исправляйте контракт: параметр должен быть std::vector<T>&, а не const&.

Ошибка №2: Путаница reserve() и «я уже могу писать в begin()».
Иногда студенты делают контейнер “вроде бы большой”, но пустой, а потом пытаются «записать по итератору». Тут важно помнить: размер (size) и ёмкость (capacity) — не одно и то же. Это не совсем тема текущей лекции, но ошибка выглядит рядом: вы получаете итератор и думаете, что он “точка записи”. На самом деле итератор корректен только там, где реально есть элементы.

Ошибка №3: Обход через cbegin/cend, а потом попытка «чуть-чуть поменять один элемент».
Это тот случай, когда const_iterator делает свою работу: он не даёт вам смешивать “чтение” и “запись” в одном участке. Исправление простое по смыслу: либо меняйте участок на begin/end, либо вынесите модификацию в отдельную функцию/цикл. Обычно второй вариант читабельнее: вы явно разделяете этапы.

Ошибка №4: for (auto x : v) и ожидание, что контейнер изменится.
Это классическая ловушка: auto x в range-for — копия, а вы меняете копию. Контейнер остаётся прежним, и вы удивляетесь, почему “ничего не работает”. Если нужно менять контейнер, используйте auto& x. Если нужно быстро и безопасно читать, используйте const auto& x.

Ошибка №5: «Алгоритм нашёл элемент, но я не могу его поменять!»
Проверьте, какими итераторами вы вызвали алгоритм. Если это были cbegin/cend, алгоритм честно вернёт const_iterator. Это не баг алгоритма, это ваш осознанный режим “только чтение”. Для модификации нужно вызывать алгоритм на изменяемом диапазоне (begin/end) или менять дизайн участка кода, чтобы не модифицировать элементы через результат поиска.

1
Задача
C++ SELF, 58 уровень, 3 лекция
Недоступна
Сумма под пломбой
Сумма под пломбой
1
Задача
C++ SELF, 58 уровень, 3 лекция
Недоступна
Поиск без правок
Поиск без правок
1
Задача
C++ SELF, 58 уровень, 3 лекция
Недоступна
Апгрейд чётных
Апгрейд чётных
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ