Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.syntblaze.com/llms.txt

Use this file to discover all available pages before exploring further.

A range-based for loop is a C++11 language construct that provides a simplified syntax for iterating over elements of a container or any range defined by begin() and end() iterators. It acts as syntactic sugar, abstracting away explicit iterator management and index manipulation by operating directly on the dereferenced elements yielded by the range expression.

Syntax

for ( init-statement_opt range_declaration : range_expression ) {
    loop_statement
}
  • init-statement_opt (Optional, introduced in C++20): A declaration or expression that initializes variables scoped to the lifetime of the loop. In C++ grammar, an init-statement inherently includes its terminating semicolon (e.g., int i = 0;).
  • range_declaration: A declaration of a named variable. The type of this variable is deduced from the elements of the sequence, typically utilizing auto combined with reference (&, &&) and const qualifiers.
  • range_expression: An expression that evaluates to a sequence. This can be a braced-init-list, a built-in array, or an object of a type that exposes begin() and end() iterators (either as member functions or via Argument-Dependent Lookup).
  • loop_statement: The body of the loop executed for each element.

Compiler Expansion

The C++ standard dictates that the compiler expands the range-based for loop into an equivalent traditional for loop. A standard range-based for loop translates to the following underlying structure:
{
    init-statement_opt // If provided (C++20)
    auto && __range = range_expression;
    auto __begin = begin_expr;
    auto __end = end_expr;
    for ( ; __begin != __end; ++__begin ) {
        range_declaration = *__begin;
        loop_statement
    }
}
Note: As of C++17, the types of __begin and __end are allowed to differ, enabling compatibility with sentinel values used in ranges.

Resolution of begin_expr and end_expr

The compiler resolves the iterators based on the type of __range:
  1. Built-in Arrays: If __range is an array of known bound, it resolves to pointers to the first element and one past the last element (__range and __range + bound).
  2. Member Functions: If __range is a class type possessing .begin() and .end() member functions, it resolves to __range.begin() and __range.end().
  3. Non-Member Functions: If the class lacks these members, the compiler uses Argument-Dependent Lookup (ADL) to find non-member begin(__range) and end(__range).

Object Lifetimes and C++23 Extension

Understanding the compiler expansion is critical for grasping object lifetimes. Prior to C++23, only the object returned by the range_expression itself (__range) had its lifetime extended to the end of the loop. Any other temporaries created within the evaluation of the range_expression were destroyed at the end of the full expression. This frequently caused dangling references. For example, in for (auto c : get_optional_string().value()), the temporary std::optional returned by get_optional_string() would be destroyed before the loop body executed, leaving the hidden __range variable bound to a reference inside a destroyed object. As of C++23 (P2718R0), the lifetimes of all temporaries materialized during the evaluation of the range_expression are extended to the end of the loop, safely preserving them for the duration of the iteration.

Range Declaration Type Deduction

The range_declaration dictates how elements are bound during iteration. Because it relies on the dereferenced iterator (*__begin), the choice of type qualifiers directly impacts memory semantics:
// 1. By Value
for (auto x : range) 
Performs copy-initialization for each element. Depending on the iterator’s reference type, this may invoke a copy constructor, a move constructor (e.g., if iterating over a range with std::move_iterator), or elide the operation entirely if the iterator yields a prvalue (due to guaranteed copy elision). x is an independent, mutable object. When used with proxy objects (like std::vector<bool>), this initializes x with the proxy itself, which still correctly reads or mutates the underlying container memory.
// 2. Lvalue Reference
for (auto& x : range) 
Binds x directly to the memory location of the element. Allows in-place mutation of the underlying range elements. Fails to compile if the range yields prvalues or temporary proxy objects.
// 3. Const Lvalue Reference
for (const auto& x : range) 
Binds x to the element as a read-only reference. Prevents copying while enforcing immutability. Extends the lifetime of materialized temporaries if the range yields prvalues.
// 4. Forwarding Reference (Universal Reference)
for (auto&& x : range) 
Binds to both lvalues and rvalues. This is essential in generic programming where the exact range type is unknown. It ensures that standard container elements are bound by reference (avoiding copies), while also successfully binding to temporary proxy objects returned by containers like std::vector<bool> (which would fail to bind to auto&).
Master C++ with Deep Grasping Methodology!Learn More