Menu

Цикл for на основе диапазона в C++: синтаксис, auto и ссылки

Цикл for на основе диапазона в C++ простыми словами: чистый обход массивов, векторов, строк и map, почему стоит использовать auto& и const auto&, а также как избежать ловушек с копированием и инвалидацией итераторов.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

«Для каждого элемента сделай это»

Классический счётный цикл for хорош, когда у вас есть индекс, которым нужно управлять. Но чаще всего индекс вам на самом деле не нужен - вы просто хотите обработать каждый элемент контейнера. Писать ради этого for (int i = 0; i < v.size(); i++) многословно, а i всего в одной ошибке смещения на единицу от чтения за границами.

В C++11 появился цикл for на основе диапазона именно для этого. Вы называете переменную, указываете на контейнер, и цикл сам проходит по каждому элементу:

Ни индекса, ни .size(), ни границ, в которых можно ошибиться. Читайте это как «для каждого s в scores». Это работает с обычными массивами, std::vector, std::string, std::map и всем остальным, что предоставляет begin() и end().

Пусть auto сам выберет тип

Явное указание типа элемента работает, но оно хрупкое - смените тип контейнера, и каждый цикл тоже придётся менять. Сочетайте цикл for на основе диапазона с auto, и компилятор сам выведет тип элемента:

Однако здесь есть скрытая цена. Обычный auto name выводит string и копирует каждый элемент в name на каждом проходе. Для int это бесплатно; для string или большой структуры это впустую потраченное выделение памяти на каждой итерации. Решение - ссылки, и это следующее, что нужно понять.

Изменяем на месте с помощью auto&

Если вы пишете auto x, вы получаете копию - поэтому присваивание x меняет копию, а не контейнер. Обратите внимание на эту ловушку:

Умножение незаметно ничего не делает, потому что n - одноразовая копия. Чтобы действительно править элементы, берите их по ссылке с помощью auto&:

Единственный & - вот вся разница между «смотри, но не трогай» и «правь на месте». Если вы когда-нибудь задумаетесь, почему ваши изменения исчезают, причина почти всегда в этом.

Чтение без копирования: const auto&

Когда вам нужно лишь читать элементы, но они дороги в копировании, используйте const auto&. Ссылка избавляет от копии, а const документирует (и обеспечивает), что вы ничего не измените:

Хорошее эмпирическое правило:

for (auto x : c)         // копия    - дешёвые типы (int, char, указатели)
for (auto& x : c)        // правка   - вы хотите менять элементы
for (const auto& x : c)  // чтение   - тяжёлые типы, которые только просматриваете

По умолчанию используйте const auto& при чтении и auto& при записи. Прибегайте к обычному auto только для по-настоящему маленьких, дешёвых в копировании типов.

Обход map и pair

Цикл for на основе диапазона по std::map выдаёт вам std::pair для каждой записи с .first (ключом) и .second (значением). Начиная с C++17, structured bindings позволяют распаковать этот pair в две именованные переменные прямо в заголовке цикла:

[name, age] намного понятнее, чем повторять entry.first и entry.second повсюду. Сохраняйте const auto& и здесь - ключ записи map это string, поэтому копировать каждый pair было бы расточительно.

Ловушка: не меняйте размер во время обхода

Самая большая ловушка - менять размер контейнера, пока цикл for на основе диапазона его обходит. Вызов push_back, erase, insert или clear может перераспределить внутреннее хранилище и инвалидировать внутренние итераторы цикла - результат это неопределённое поведение, то есть падения или мусор, а не дружелюбная ошибка:

vector<int> v = {1, 2, 3};
for (int x : v) {
    v.push_back(x);   // НЕОПРЕДЕЛЁННОЕ ПОВЕДЕНИЕ - перераспределение инвалидирует диапазон
}

Если во время обработки нужно добавлять или удалять элементы, переключитесь на цикл for на основе индекса или итератора и управляйте границами сами, либо постройте отдельный контейнер с результатом и подмените его потом. Две ловушки поменьше из того же ряда: никогда не привязывайте цикл for на основе диапазона к временному объекту, который сразу умирает (for (auto x : makeVector()) нормально, но for (auto& x : someObj.getTempVector()) может «повиснуть»), и помните, что for (auto& c : myString) позволяет изменять отдельные символы на месте.

Далее: функции

Цикл for на основе диапазона наводит порядок в обходе, а выбор между auto / auto& / const auto&, который вы только что освоили, напрямую переносится на один из важнейших инструментов C++. Далее мы упакуем логику в переиспользуемые функции - дадим коду имя, параметры и возвращаемое значение, чтобы вызывать его откуда угодно вместо того, чтобы повторяться.

Часто задаваемые вопросы

Что такое цикл for на основе диапазона в C++?

Цикл for на основе диапазона проходит по каждому элементу контейнера (массива, vector, string, map и т. д.), не заставляя вас управлять индексом или итератором. Синтаксис: for (auto x : container) { ... }. Он появился в C++11 и является самым чистым способом сказать «сделай это для каждого элемента».

Когда использовать auto& вместо auto в цикле for на основе диапазона?

Используйте auto& x, когда хотите изменять элементы на месте, и const auto& x, когда только читаете их, но хотите избежать копирования (важно для string, vector или крупных объектов). Обычный auto x создаёт копию на каждой итерации - это нормально для дешёвых типов вроде int, но расточительно в остальных случаях.

Можно ли менять размер вектора внутри цикла for на основе диапазона в C++?

Нет. Вызов push_back, erase, insert или clear для контейнера, по которому вы итерируетесь, инвалидирует внутренние итераторы цикла и является неопределённым поведением - программа может упасть или незаметно повредить данные. Если нужно добавлять или удалять элементы во время обхода, используйте вместо этого цикл for на основе индекса или итератора.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ