How C++ Range-Based for Loops Work Under the Hood
Range-based for loops, introduced in C++11, provide a
clean and safe syntax for iterating over sequences, containers, and
arrays. Under the hood, this construct does not introduce new runtime
machinery; instead, it acts as syntactic sugar that the compiler expands
into a standard iterator-based for loop. By querying the
beginning and end of a target sequence and stepping through elements
using iterators and dereference operations, the compiler generates
low-level iteration code identical to manual pointer or iterator
arithmetic.
The Compiler Expansion
When the compiler encounters a range-based for loop
written as:
for (range_declaration : range_expression) {
loop_statement
}It expands the code into an equivalent traditional iteration pattern. Conceptually, since C++17, the expansion looks like this:
{
auto&& __range = range_expression;
auto __begin = begin_expr;
auto __end = end_expr;
for (; __begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}Key Under-the-Hood Mechanisms
Universal Reference and Lifetime Extension:
The expressionauto&& __range = range_expression;binds the container or sequence to a forwarding reference. Ifrange_expressionproduces a temporary object (an rvalue), its lifetime is extended to the end of the entire loop construct, preventing immediate dangling references.Resolving
begin()andend():
The compiler determinesbegin_exprandend_exprbased on the type of__range:- Raw Arrays: If
__rangeis an array of known bounds,begin_exprbecomes__rangeandend_exprbecomes__range + __bound. - Member Functions: If the type of
__rangedefines members namedbegin()andend(), the compiler calls__range.begin()and__range.end(). - Argument-Dependent Lookup (ADL): If member
functions do not exist, the compiler looks for non-member
begin(__range)andend(__range)functions in the associated namespaces via ADL.
- Raw Arrays: If
Iterator Dereferencing and Incrementing:
During each iteration, the loop createsrange_declarationby dereferencing the current iterator (*__begin). The iterator is then advanced via the pre-increment operator (++__begin), and the loop continues until the condition__begin != __endevaluates tofalse.
Evolution Across C++ Standards
- C++11 and C++14: The types of
__beginand__endwere required to be identical. - C++17 Sentinels: C++17 relaxed the requirement for
identical types, allowing
__beginand__endto have different types. This enables the use of sentinel types, such as a null-terminator sentinel for C-strings, where the end condition is determined dynamically rather than precomputed. - C++20 Init-Statement: C++20 added an optional
initialization statement:
for (init-statement; range_declaration : range_expression). This allows objects to be declared within the loop's outer scope, mitigating dangling reference bugs when chaining accessors on temporary objects.
Performance and Assembly Output
Because range-based for loops are strictly a
compile-time transformation, they incur zero runtime overhead compared
to hand-written iterator or index-based loops. In optimized builds
(-O2 or -O3), compilers inline the iterator
operations—such as operator*, operator++, and
operator!=—resulting in identical machine instructions to
raw pointer traversals.