# Глава 6. Итераторы Стандартная библиотека C++ содержит три важных компонента: - Контейнеры, реализующие различные структуры данных. - Алгоритмы для работы с контейнерами: сортировка, поиск и многое другое. - Итераторы — связующее звено между контейнерами и алгоритмами. Начнем с итераторов. А в следующих главах взглянем на многообразие контейнеров и алгоритмов стандартной библиотеки. ## Что такое итератор **Итератор** (iterator) — это абстракция для доступа к элементам контейнера. Через объект итератора можно обходить элементы в цикле или работать с ними поштучно. Итератор — это распространенный [паттерн проектирования.](https://ru.wikipedia.org/wiki/%D0%98%D1%82%D0%B5%D1%80%D0%B0%D1%82%D0%BE%D1%80_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)) На уровне стандартной библиотеки он реализован и в других языках, например в C# и Rust. Какую задачу решает паттерн «итератор»? Представьте, что вы автор стандартной библиотеки C++. Вы создали набор контейнеров, среди которых массивы, хеш-таблицы, списки, очереди. Но контейнеров самих по себе недостаточно. Нужно предоставить алгоритмы для работы с ними: поиск, сортировку, слияние, фильтрацию и другие функции. Как это сделать? Можно каждый из алгоритмов превратить в метод класса контейнера. Но у такого подхода есть недостатки: - Перегруженный интерфейс класса. Придется добавлять сотни публичных методов! - Дублирование одних и тех же алгоритмов для разных контейнеров. - Плохая масштабируемость. Чтобы добавить алгоритм, придется дополнить все контейнеры новым методом. - Нет разграничения ответственности между кодом, реализующим структуру данных, и кодом алгоритма для обработки данных. Зачем классу `std::string` знать, как устроен бинарный поиск? Альтернативный подход заключается в использовании итераторов. Каждому классу контейнера соответствует свой класс итератора. Например, итератор по массиву или итератор по списку. Объект итератора имеет доступ к элементам контейнера и умеет их перебирать. Алгоритмы для работы с контейнерами оформляются в виде свободных функций, которые принимают на вход итераторы. Это дает преимущества: - Интерфейс класса контейнера содержит только самое необходимое. Таким классом удобно пользоваться. - Отсутствует дублирование кода. - Гибкость. Алгоритмы можно применять к контейнеру целиком или к диапазону элементов. - Разделение обязанностей между кодом класса контейнера и кодом функции, реализующей алгоритм. Такое разделение позволяет развивать контейнеры и алгоритмы независимо друг от друга. Посмотрим, как выглядит использование связки контейнеров, алгоритмов и итераторов. Заведем строку `s` и динамический массив `v`. Оба контейнера имеют методы `begin()` и `end()`. Они возвращают итераторы на первый элемент и на позицию за последним элементом. Вызовем [функцию std::reverse(),](https://en.cppreference.com/w/cpp/algorithm/reverse) которая меняет порядок элементов на обратный. Она принимает итераторы на начало и конец диапазона, который нужно «перевернуть»: ```c++ {.example_for_playground .example_for_playground_001} std::string s = "spam"; std::vector v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; std::reverse(s.begin(), s.end()); std::reverse(v.begin(), v.end()); std::println("Reversed.\nString: {}. Vector: {}", s, v); ``` ``` Reversed. String: maps. Vector: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] ``` Итераторы полезны и для написания собственных алгоритмов. Ведь они предоставляют гибкий и _единообразный_ доступ к элементам контейнеров разных типов. А значит, функции не надо знать, с элементами какого контейнера она работает. ## Реализация итераторов в C++ У каждого контейнера есть свой тип итератора. Тип итератора по строке — `std::string::iterator`, а итератор по массиву `std::vector` — `std::vector::iterator`. Здесь полное имя типа состоит из компонентов: - `std` — пространство имен, - `vector` — класс динамически изменяемого массива, - `iterator` — класс, реализованный внутри другого класса. То есть вложенный в `vector`. Тип и реализация у всех итераторов разные, но работать с контейнерами они позволяют одинаково. Разберем это на примере цикла по строке. Вы успели познакомиться с двумя вариантами перебора строки: циклом `for` по индексам и циклом `range-for` по символам: ```c++ {.example_for_playground .example_for_playground_002} std::string s = "string"; for (std::size_t i = 0; i < s.size(); ++i) std::println("{}", s[i]); for (char c: s) std::println("{}", c); ``` Но в отличие от строки не каждый контейнер поддерживает доступ к элементам по индексам. А цикл `range-for` перебирает все элементы диапазона без возможности задания шага, начальных и конечных условий. Поэтому в ряде случаев необходим третий способ организации циклов — через итераторы. Цикл через итераторы абсолютно одинаков для строк, массивов и _любых_ контейнеров: ```c++ {.example_for_playground .example_for_playground_003} for(std::string::iterator it = s.begin(); it != s.end(); ++it) std::println("{}", *it); ``` Методы `begin()` и `end()` есть у всех контейнеров стандартной библиотеки: - `begin()` возвращает итератор, указывающий на начальный элемент. - `end()` возвращает итератор на позицию **за** последним элементом. ![Итераторы по строке](https://raw.githubusercontent.com/senjun-team/senjun-courses/refs/heads/cpp-chapter-6/illustrations/cpp/iterators.jpg) {.illustration} ## Основные действия над итераторами Есть несколько **операторов,** которые применимы к итераторам **всех** контейнеров: - `==`, `!=` — строгое сравнение на равенство и неравенство. - `*` — оператор разыменования. Он выглядит, как оператор умножения, но имеет другой смысл. Он нужен для обращения к элементу, на который указывает итератор: `std::println("{}", *it)`. Итератор, возвращаемый методом `end()`, разыменовывать нельзя. - `++` — перемещение к следующему элементу. Но есть и такие операторы, которые реализованы только для итераторов **некоторых** контейнеров: - `>`, `>=`, `<`, `<=` — сравнение «больше-меньше». Итератор больше другого, если он ближе к концу контейнера. - `--` — перемещение к предыдущему элементу: `--it`. - `+`, `-`, `+=`, `-=` — перемещение на заданное количество элементов: `it - 5` означает получение итератора на 5 элементов ближе к началу контейнера, а `it += 2` — сдвиг `it` на 2 элемента к концу. Почему эти операторы поддерживаются не везде? Дело в специфике контейнера. Например, каждый элемент односвязного списка ссылается лишь на следующий элемент и не обладает информацией о предыдущем. Поэтому к итератору контейнера `std::forward_list` применимы только операторы `==`, `!=`, `*` и `++`. В зависимости от того, какие над итератором допустимы действия, итератор относится к одной из категорий: итератор для чтения значений элементов (input), изменения значений (output), прямого итерирования (forward), двунаправленного итерирования (bidirectional) и произвольного доступа к любым элементам (random access). ![Категории итераторов](https://raw.githubusercontent.com/senjun-team/senjun-courses/refs/heads/cpp-chapter-6/illustrations/cpp/iterator_categories.jpg) {.illustration} Итераторы по каждому из стандартных контейнеров относятся к одной из этих категорий. Например, итераторы по строке являются итераторами произвольного доступа. Так выглядит изменение символов строки в цикле по итераторам: ```c++ {.example_for_playground .example_for_playground_004} std::string s = "iteration over string"; for(std::string::iterator it = s.begin(); it != s.end(); ++it) { if (*it == ' ') *it = '_'; } std::println("{}", s); ``` ``` iteration_over_string ``` Перебирать контейнеры можно и с помощью цикла `while`: ```c++ {.example_for_playground .example_for_playground_018} std::string card = "MasterCard N:5200 8282 8282 8210"; std::size_t hide_count = 12; std::string::iterator it = card.begin(); while (it != card.end() && hide_count > 0) { if (*it >= '0' && *it <= '9') { *it = '*'; --hide_count; } ++it; } std::println("{}", card); ``` ``` MasterCard N:**** **** **** 8210 ``` Напишите функцию `is_palindrome()`, которая принимает строку и определяет, является ли строка палиндромом. Палиндром — это строка, одинаково выглядящая в обоих направлениях. Например, `"eve"`, `"sum summus mus"`. Пустая строка палиндромом не считается. {.task_text} В своем решении используйте итераторы. {.task_text} ```c++ {.task_source #cpp_chapter_0060_task_0020} bool is_palindrome(std::string text) { } ``` Чтобы проверить, является ли строка палиндромом, нужно сравнить ее нулевой символ с последним, первый — с предпоследним и так до середины строки. {.task_hint} ```c++ {.task_answer} bool is_palindrome(std::string text) { if (text.empty()) return false; std::string::iterator it = text.begin(); std::string::iterator it_tail = text.end() - 1; const std::string::iterator it_end = text.begin() + text.size() / 2; while (it != it_end) { if (*it != *it_tail) return false; ++it; --it_tail; } return true; } ``` Многие алгоритмы стандартной библиотеки используют итераторы в качестве параметров функции или возвращаемого значения. Так, [std::find_if()](https://en.cppreference.com/w/cpp/algorithm/find) принимает итераторы на границы интересующего диапазона и предикат (функцию, возвращающую `true` либо `false`). Начальный элемент диапазона участвует в поиске, а последний — нет. Функция возвращает итератор на первый элемент внутри диапазона, для которого предикат вернул `true`. Если такого элемента нет, функция возвращает итератор на последний элемент диапазона. А [std::distance()](https://en.cppreference.com/w/cpp/iterator/distance) принимает итераторы на границы диапазона. Она возвращает, сколько элементов расположено между этими границами, включая начальный элемент диапазона и не включая последний. ```c++ {.example_for_playground} import std; // Параметром шаблона может быть литерал простого типа, // в данном случае любой ASCII символ template<char Sym> bool expected(char c) { return c == Sym; } // Параметр шаблона - функция-предикат template <typename Pred> void print_distance(std::string str, Pred pred) { const std::string::iterator it = std::find_if(str.begin(), str.end(), pred); if (it == str.end()) { std::println("a character cannot be found by predicate"); } else { const std::size_t d = std::distance(str.begin(), it); std::println("distance to '{}' is {}", *it, d); } } int main() { std::string menu_item = "FAQ"; print_distance(menu_item, expected<'F'>); print_distance(menu_item, expected<'A'>); print_distance(menu_item, expected<'Q'>); print_distance(menu_item, expected<'X'>); } ``` ``` distance to 'F' is 0 distance to 'A' is 1 distance to 'Q' is 2 a character cannot be found by predicate ``` Напишите шаблонную функцию `index_of()`. Функция принимает строку и предикат. Тип предиката является параметром шаблона. Функция возвращает индекс первого символа, для которого предикат вернул `true`. Если такого символа нет, функция должна бросить исключение `std::runtime_error`. {.task_text} В своем решении используйте алгоритмы `std::find_if()` и `std::distance()`. {.task_text} ```c++ {.task_source #cpp_chapter_0060_task_0010} ``` У шаблона единственный параметр — тип предиката. Назовем его `Fn`. Тогда функция будет выглядеть так: `template<class Fn> std::size_t index_of(std::string s, Fn pred)`. Внутри функции нужно вызвать `std::find_if()` от итераторов на начало и конец строки и предиката `pred`. Если итератор, который вернет `std::find_if()`, равен `s.end()`, нужно бросить исключение. Иначе вернуть расстояние от начала строки до этого итератора. Для этого вызовите функцию `std::distance()`. {.task_hint} ```c++ {.task_answer} template<class Fn> std::size_t index_of(std::string s, Fn pred) { std::string::iterator it = std::find_if( s.begin(), s.end(), pred); if (it == s.end()) throw std::runtime_error("not found"); return std::distance(s.begin(), it); } ``` ## Константные итераторы Взгляните на этот код. Он не скомпилируется. Удостоверьтесь в этом, запустив его в плэйграунде. ```c++ {.example_for_playground .example_for_playground_005} const std::string s = "string"; for(std::string::iterator it = s.begin(); it != s.end(); ++it) std::println("{}", *it); ``` Дело в том, что строка `s` константная. А для константных контейнеров методы `begin()` и `end()` вместо обычного итератора возвращают тип `const_iterator`. Через него элементы доступны только на чтение. Исправим ошибку компиляции заменой типа итератора `it`: ```c++ {.example_for_playground .example_for_playground_006} for(std::string::const_iterator it = s.begin(); it != s.end(); ++it) std::println("{}", *it); ``` Как это работает? Почему метод `begin()` возвращает итераторы разных типов? Все просто: методы класса можно перегружать. Причем перегрузка возможна не только по уникальному набору параметров, но и по квалификатору `const`, который относится целиком к методу. Методы объявляют константными, если внутри них не изменяются поля класса. У метода `begin()` класса `vector` есть две перегрузки. И при вызове метода от константного объекта класса компилятор выбирает перегрузку, помеченную `const`: ```c++ template<typename T, typename Alloc> class vector { // ... iterator begin() { return iterator(this->_M_impl._M_start); } const_iterator begin() const { return const_iterator(this->_M_impl._M_start); } // ... }; ``` Даже если сам контейнер не константный, зачастую безопаснее работать с ним через константные итераторы. Для получения константных итераторов даже от неконстантного контейнера предусмотрены методы `cbegin()` и `cend()`. Используйте их в случаях, не требующих изменения элементов. Это поможет: - Исключить случайную модификацию контейнера. - Подчеркнуть намерение только читать значения. Код станет яснее. ```c++ {.example_for_playground .example_for_playground_007} std::string s = "string"; // Не константная строка for(std::string::const_iterator it = s.cbegin(); it != s.cend(); ++it) std::println("{}", *it); ``` В C++ есть квалификатор типа `const`, делающий объект иммутабельным (неизменяемым). Напрашивается вопрос: зачем потребовался тип `std::string::const_iterator`, если можно написать так: `const std::string::iterator it`? Между константой `const std::string::iterator` и классом константного итератора `std::string::const_iterator` существует принципиальная разница. Как и любую константу, константный итератор нельзя менять: ```c++ {.example_for_playground .example_for_playground_008} const std::string::iterator it = s.begin(); ++it; // Упс! ``` Зато можно менять значение объекта, на который он _указывает_. Ведь сам итератор при этом не меняется. Меняется значение элемента контейнера. ```c++ {.example_for_playground .example_for_playground_009} const std::string::iterator it = s.begin(); *it = 'A'; // Ок ``` В случае с `const_iterator` ситуация обратная. Его можно менять: ```c++ {.example_for_playground .example_for_playground_010} std::string::const_iterator it = s.cbegin(); ++it; // Ок ``` Но через такой итератор нельзя модифицировать значение объекта, на который он указывает: ```c++ {.example_for_playground .example_for_playground_011} std::string::const_iterator it = s.cbegin(); *it = 'A'; // Ошибка ``` И, конечно, в обоих случаях через итератор удастся прочитать значение элемента. ```c++ {.example_for_playground .example_for_playground_012} const std::string::iterator c_it = s.begin(); std::string::const_iterator it_c = s.cbegin(); std::println("{} {}", *c_it, *it_c); // Ок ``` Через сочетание квалификатора `const` и типа `const_iterator` можно получить итератор, через который нельзя менять вообще ничего. Только читать: ```c++ {.example_for_playground .example_for_playground_013} const std::string::const_iterator it = s.cbegin(); ++it; // Ошибка *it = 'A'; // Ошибка char c = *it; // Ок ``` ## Обратные итераторы При работе с некоторыми контейнерами может потребоваться доступ к элементам в обратном порядке. Для этого есть тип итератора `reverse_iterator`, а также методы контейнера `rbegin()` и `rend()`: - `rbegin()` возвращает `reverse_iterator`, который указывает на последний элемент. - `rend()` возвращает `reverse_iterator`, указывающий на позицию **перед** первым элементом. При инкременте `reverse_iterator` смещается ближе к началу контейнера. ![Обратные итераторы по строке](https://raw.githubusercontent.com/senjun-team/senjun-courses/refs/heads/cpp-chapter-6/illustrations/cpp/reverse_iterators.jpg) {.illustration} Так выглядит обратный проход по строке: ```c++ {.example_for_playground .example_for_playground_014} std::string s = "string"; for(std::string::reverse_iterator it = s.rbegin(); it != s.rend(); ++it) std::println("{}", *it); ``` В этом примере мы не модифицировали элементы строки. Поэтому правильнее было бы заменить методы `rbegin()` и `rend()` на `crbegin()` и `crend()`, а тип `std::string::reverse_iterator` на `std::string::const_reverse_iterator`. В функцию `hide_password()` приходит строка с логином и паролем вида `login:password`. Функция возвращает строку со скрытым паролем, в которой каждый символ пароля заменен на `'*'`. {.task_text} Напишите тело этой функции с использованием обратных итераторов. {.task_text} ```c++ {.task_source #cpp_chapter_0060_task_0070} std::string hide_password(std::string logpass) { } ``` С помощью обратных итераторов организуйте цикл по строке. Замените все символы на `'*'`, пока не достигните символа `':'`. {.task_hint} ```c++ {.task_answer} std::string hide_password(std::string logpass) { for (std::string::reverse_iterator it = logpass.rbegin(); it != logpass.rend(); ++it) { if (*it == ':') break; *it = '*'; } return logpass; } ``` Чтобы получить из обратного итератора обычный, предусмотрен метод `base()`. Он возвращает обычный итератор на элемент, который на одну позицию ближе к концу контейнера. Это нужно, чтобы итератор `rbegin()` можно было соотнести `begin()`, а `rend()` — итератору `end()`. ```c++ {.example_for_playground .example_for_playground_015} std::string s = "string"; std::string::reverse_iterator rit = s.rbegin(); // g ++rit; // n std::string::iterator it = rit.base(); // g ``` Напишите тело функции `find_last()`, которая пару _обратных_ итераторов на начало и конец диапазона строки и символ. Функция должна вернуть итератор типа `std::string::iterator` на _последнее_ вхождение символа в строку либо итератор на конец диапазона, если символ не найден. {.task_text} ```c++ {.task_source #cpp_chapter_0060_task_0030} std::string::iterator find_last(std::string::reverse_iterator rbegin, std::string::reverse_iterator rend, char c) { } ``` С помощью обратных итераторов организуйте цикл по строке. Как только символ, на который указывает обратный итератор `rit`, совпадет с искомым, верните `(rit + 1).base()`. {.task_hint} ```c++ {.task_answer} std::string::iterator find_last(std::string::reverse_iterator rbegin, std::string::reverse_iterator rend, char c) { for(std::string::reverse_iterator rit = rbegin; rit != rend; ++rit) { if(*rit == c) return (rit + 1).base(); } return rbegin.base(); } ``` Если на этом моменте вы почувствовали острое нежелание всякий раз писать длинные типы итераторов, то у нас хорошие новости. В C++ есть автоматический вывод типов! ## Ключевое слово auto Если всегда указывать тип итератора, код выглядит громоздким: ```c++ for(std::string::iterator it = s.begin(); it != s.end(); ++it) std::println("{}", *it); ``` Поэтому вместо типа зачастую пишут [ключевое слово auto.](https://en.cppreference.com/w/cpp/language/auto) Оно было введено в C++11 и позволяет компилятору самостоятельно определять тип переменной на основании того, как она инициализируется. Это называется автоматическим выводом типа. ```c++ {.example_for_playground .example_for_playground_016} for(auto it = s.begin(); it != s.end(); ++it) std::println("{}", *it); ``` ## Инвалидация итераторов В некоторых случаях итератор может перестать указывать туда, куда должен. Это называется [инвалидацией.](https://en.cppreference.com/w/cpp/container#Iterator_invalidation) В зависимости от типа контейнера к инвалидации итератора приводят разные причины. Одна из причин, по которой итератор по строке становится невалидным — это удаление символа, на который или после которого указывает итератор. Заведем итератор на элемент строки. А затем удалим из строки все символы `'t'` с помощью функции [std::erase().](https://en.cppreference.com/w/cpp/container/vector/erase2) После этого итератор станет невалидным: ```c++ {.example_for_playground .example_for_playground_017} std::string s = "iterator invalidation"; auto it = std::find(s.begin(), s.end(), 't'); std::erase(s, 't'); // Инвалидация std::println("{}", *it); // Неопределенное поведение ``` Обращение к значению, на которое указывает невалидный итератор, в стандарте C++ относится к **неопределенному поведению** (UB, undefined behaviour). Программа, в которой допущен UB, остается синтаксически корректной. Но она может вести себя непредсказуемо. Если не сразу, то при переносе с одной системы на другую, при смене компилятора или его версии. Непредсказуемое поведение может привести к чему угодно: к падению программы, странным ошибкам, повреждению данных, с которыми работает программа. Старайтесь не допускать в своем коде UB. А значит, будьте осторожны при работе с итераторами и следите, чтобы они не инвалидировались. В стандарте C++ [перечислены](https://timsong-cpp.github.io/cppwp/n4950/string.require#4) действия, которые потенциально могут привести к инвалидации итератора по строке: - Передача строки по неконстантной ссылке в качестве аргумента функции из стандартной библиотеки. - Вызов неконстантного метода строки кроме [некоторых](https://timsong-cpp.github.io/cppwp/n4950/string.require#4.2) методов вроде `begin()`, `end()`, `rbegin()`, `rend()`. При работе со строкой к UB приводят и другие действия. Например: - Разыменование итератора, возвращаемого методом `end()`: `*s.end()`. - Разыменование итератора, указывающего перед первым элементом: `*(--s.begin())`. Есть ли в этом коде UB? `Y/N`. {.task_text} ```c++ std::string title = "Discussion"; auto it = title.begin(); title = ""; ``` ```consoleoutput {.task_source #cpp_chapter_0060_task_0040} ``` Итератор `it` инвалидируется на 3-ей строке. Но обращения к нему не происходит, поэтому UB нет. {.task_hint} ```cpp {.task_answer} N ``` Произойдет ли в этом коде инвалидация итератора? `Y/N`. {.task_text} [Метод строки erase()](https://en.cppreference.com/w/cpp/string/basic_string/erase) удаляет символ, на который указывает итератор, и _возвращает_ итератор на следующий за ним символ либо на `end()`, если удаленный символ был последним. {.task_text} ```c++ std::string text = "See also"; for (auto it = text.begin(); it != text.end(); ++it) { if (*it == ' ') text.erase(it); } ``` ```consoleoutput {.task_source #cpp_chapter_0060_task_0050} ``` Цикл перебирает элементы от начала и до конца строки. В процессе некоторые элементы удаляются. Метод `erase()` удаляет элемент, на который указывает итератор. Соответственно итератор в этот момент инвалидируется. Метод возвращает итератор на следующий элемент, но в коде допущена ошибка: этот итератор никак не используется. {.task_hint} ```cpp {.task_answer} Y ``` ## Строка — это контейнер? Все примеры использования итераторов и алгоритмов мы приводили применительно к типу `std::string`. Но считается ли строка полноценным контейнером? Строки во многом схожи с контейнерами стандартной библиотеки. Но у них есть ограничения, о которых вы узнаете позже. Из-за этого строки иногда называют [псевдо-контейнерами](https://en.cppreference.com/w/cpp/container) (pseudo container). Напишите функцию `rearrange_words()`. Она принимает строку, которая состоит из разделенных пробелами слов. Функция должна вернуть строку, содержащую те же слова, но в обратном порядке. Например, строка `not a bug` превратится в `bug a not`. {.task_text} Строка начинается и заканчивается словом, а не пробелом. Одно слово отделяется от другого единственным пробелом. {.task_text} Воспользуйтесь функциями [std::find()](https://en.cppreference.com/w/cpp/algorithm/find) и [std::reverse().](https://en.cppreference.com/w/cpp/algorithm/reverse) {.task_text} Эта задача имеет короткое и изящное решение. Если оно не приходит вам в голову, прочтите подсказку. {.task_text} ```c++ {.task_source #cpp_chapter_0060_task_0060} std::string rearrange_words(std::string s) { } ``` Сначала разверните строку целиком. Строка `not a bug` превратится в `gub a ton`. Затем разверните каждое слово по отдельности: `bug a not`. {.task_hint} ```c++ {.task_answer} std::string rearrange_words(std::string s) { std::reverse(s.begin(), s.end()); auto it_word = s.begin(); while (true) { auto it_space = std::find(it_word, s.end(), ' '); std::reverse(it_word, it_space); if (it_space == s.end()) break; it_word = it_space + 1; } return s; } ``` ---------- ## Резюме - Итератор — это абстракция для доступа к элементам контейнера. - Итераторы позволяют единообразно работать с элементами контейнеров разных типов. - Если доступ к элементам контейнера нужен только для чтения, используйте константный итератор. - Для прохода по контейнеру в обратном порядке используйте обратные итераторы. - Ключевое слово `auto` можно использовать для автоматического вывода типа переменной. - Обращение к невалидному итератору — это UB (undefined behaviour).

Следующие главы находятся в разработке

Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!