ядро языка · синтаксисcore language · syntax

range-based for

Цикл по всем элементам контейнера или массива без явного итератора и условия выхода. Синтаксический сахар, который компилятор целиком разворачивает в обычный for с begin()/end().A loop over every element of a container or array without an explicit iterator or exit condition. Syntactic sugar that the compiler fully expands into an ordinary for loop with begin()/end().

часто используетсяcommonly used нулевой overheadzero overhead

Что этоWhat it is

for (декларация : выражение-диапазона) тело — форма цикла, которая перебирает все элементы диапазона, для которого определены begin()/end() (как свободные функции или методы), от первого до последнего, без ручного объявления итератора.for (declaration : range-expression) body — a loop form that iterates over every element of a range for which begin()/end() are defined (as free functions or member functions), from first to last, without manually declaring an iterator.

до C++11before C++11
for (std::vector<int>::iterator it = v.begin();
     it != v.end(); ++it) {
    std::cout << *it;
}
range-based for
for (int x : v) {
    std::cout << x;
}

Зачем ввелиWhy it was introduced

Итератор вручную — это шаблон кода, который почти всегда одинаков и почти всегда лишний:A hand-written iterator is boilerplate that is almost always identical and almost always unnecessary:

01

Убрать шаблонный код и его багиRemove boilerplate and its bugs

Опечатка вроде it != v.end() - 1 или сравнение итераторов разных контейнеров — классические источники багов ручного цикла. Range-based for устраняет саму возможность ошибиться в условии выхода.A typo like it != v.end() - 1, or comparing iterators from different containers, are classic sources of bugs in a hand-written loop. Range-based for eliminates the very possibility of getting the exit condition wrong.

02

Единый синтаксис для любых диапазоновA uniform syntax for any range

Массивы, контейнеры STL, инициализатор-листы, пользовательские типы с begin()/end() — все перебираются одной и той же конструкцией, без знания конкретного типа итератора.Arrays, STL containers, initializer lists, custom types with begin()/end() — all of them are iterated with the same construct, without knowing the concrete iterator type.

03

Соответствие практике других языковCatching up with other languages' practice

К 2011 году for-each-цикл уже был стандартом де-факто в Java, Python, C# — C++ отставал синтаксически, хотя обладал всей инфраструктурой (итераторы) для реализации того же самого.By 2011, a for-each loop was already the de facto standard in Java, Python, and C# — C++ was lagging syntactically, despite already having all the infrastructure (iterators) needed to implement the very same thing.

Как это работает под капотомHow it works under the hood

Компилятор разворачивает range-based for в обычный for с временными переменными __begin/__end — ровно то же самое, что писалось бы руками:The compiler expands range-based for into an ordinary for loop with temporary variables __begin/__end — exactly what you would write by hand:

for (auto& x : container) { body }

// эквивалент, который генерирует компилятор:// the equivalent the compiler generates:
{
    auto&& __range = container;
    auto __begin = begin(__range);   // ADL: begin(__range) или __range.begin()// ADL: begin(__range) or __range.begin()
    auto __end   = end(__range);     // приоритет — member begin()/end(),// priority goes to member begin()/end(),
                                      // иначе свободная функция через ADL// otherwise a free function found via ADL
    for (; __begin != __end; ++__begin) {
        auto& x = *__begin;
        body
    }
}

Для массива фиксированного размера begin/end — это просто container и container + N, известные на этапе компиляции. Для контейнера с пользовательским итератором — ровно то же, что и в ручном цикле: никакого дополнительного рантайм-кода range-based for не добавляет, оптимизатор видит идентичный IR.For a fixed-size array, begin/end are just container and container + N, known at compile time. For a container with a custom iterator, it's exactly the same as a hand-written loop — range-based for adds no additional runtime code, and the optimizer sees identical IR.

ГраблиGotchas

for (auto x : v) — копия элемента на каждой итерацииa copy of the element on every iteration

Без & декларация auto x копирует каждый элемент диапазона. Для тяжёлых типов (строки, векторы, кастомные объекты) это незаметный, но реальный overhead — почти всегда нужен const auto& для чтения или auto& для изменения.Without &, the declaration auto x copies every element of the range. For heavy types (strings, vectors, custom objects) this is an invisible but very real overhead — you almost always want const auto& for reading or auto& for mutating.

Временный объект как диапазон — UB после C++11, well-defined с C++23A temporary as the range — UB after C++11, well-defined since C++23

for (auto x : getVector()) — временный результат getVector() живёт всё время выполнения цикла благодаря продлению жизни через auto&& __range, поэтому сам цикл безопасен. Ловушка — если тело диапазона возвращает ссылку/итератор на что-то временное внутри выражения (например, метод, возвращающий диапазон на данные временного подобъекта) — тогда UB остаётся и в C++23-исправлении это не учтено.for (auto x : getVector()) — the temporary result of getVector() lives for the entire duration of the loop thanks to lifetime extension via auto&& __range, so the loop itself is safe. The trap is when the range expression returns a reference or iterator into something temporary nested inside it (for example, a method returning a range over data owned by a temporary subobject) — that case remains UB, and the C++23 fix doesn't cover it.

Изменение контейнера внутри цикла инвалидирует итераторыModifying the container inside the loop invalidates iterators

for (auto& x : v) v.push_back(x); — та же проблема, что и с ручными итераторами: push_back может реаллоцировать буфер вектора, и __begin/__end внутри цикла станут dangling. Синтаксис короче, инвалидация итераторов работает так же, как раньше.for (auto& x : v) v.push_back(x); — the same problem as with hand-written iterators: push_back may reallocate the vector's buffer, leaving __begin/__end inside the loop dangling. The syntax got shorter, but iterator invalidation works exactly as it always did.