«Для каждого элемента сделай это»
Классический счётный цикл 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 на основе индекса или итератора.