C++ • Control Flow

Range-Based For Loops — Use range-based for loops for containers

Iterate over every element in a container with concise, expressive syntax that eliminates off-by-one errors.

Programming braces with a command terminal and binary digits.
Inside this lesson 10 sections 5 questions 5 figures

SECTION 1

Historical Context & Motivation

Before C++11, iterating over the elements of a container required either index-based loops with explicit bounds or verbose iterator pairs. Both approaches placed the burden on the programmer to manage loop variables, dereference iterators, and guard against off-by-one errors—boilerplate that obscured the intent of the code. Languages such as Java (the enhanced for-each loop, introduced in Java 5) and Python (the for x in collection idiom) had already demonstrated that a simpler syntax dramatically reduced iteration bugs while improving readability. The C++ standards committee recognized this gap and introduced the range-based for loop as a first-class language feature in the C++11 standard, published in 2011.

1998 • C++98 Standard

The first ISO C++ standard formalizes the Standard Template Library (STL). Container iteration relies on explicit iterator pairs: for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it).

2005 • Boost.FOREACH Macro

The Boost libraries introduce the BOOST_FOREACH macro, providing a convenient iteration shorthand. Although widely adopted, its macro-based implementation has limitations in type deduction and debuggability.

2011 • C++11 Range-Based For

The C++11 standard introduces the range-based for loop as a core language construct. The syntax for (auto& x : container) becomes idiomatic C++.

2017 • C++17 Init-Statements & Structured Bindings

C++17 adds structured bindings (auto& [key, value]), allowing range-based for loops to destructure pairs and tuples directly from associative containers.

2020 • C++20 Ranges Library

The Ranges library (std::ranges) introduces lazy views and composable range adaptors, extending the range-based for paradigm with pipelines such as views::filter and views::transform.

The central question the range-based for loop addresses is deceptively simple: how can C++ let a programmer express "do something with every element in this collection" without forcing them to manage iterators, indices, or sentinel values? The answer reshapes how modern C++ code is written, shifting the focus from iteration mechanics to the operation itself.

SECTION 2

Core Principles & Definitions

The range-based for loop is syntactic sugar that the compiler expands into a traditional iterator-based loop. Understanding its underlying requirements and behavior ensures you use it correctly across all container types and avoid subtle pitfalls related to copying, lifetime, and mutability.

1

Syntax Structure

The general form is for (declaration : range-expression) statement. The declaration introduces a loop variable that takes the value (or reference) of each element in the range.
2

Range Requirements

A valid range is any expression for which std::begin() and std::end() return iterators. This includes arrays, STL containers, initializer lists, and user-defined types with begin/end member functions.
3

Value vs. Reference Semantics

Declaring the loop variable by value (auto x) copies each element. Declaring by reference (auto& x) avoids copies and allows in-place modification. Using const auto& prevents both copies and modifications.
4

Compiler Expansion

The compiler rewrites for (auto& x : c) into a loop that creates a begin iterator and an end sentinel, incrementing the iterator and dereferencing it on each iteration until the sentinel is reached.
5

No Index Access

The range-based for loop does not expose a counter or index variable. If you need the position of each element, you must maintain a separate counter or use an index-based loop instead.
✦

KEY TAKEAWAY

Think of a range-based for loop like a conveyor belt in a factory: each item passes before you automatically, and you simply perform your operation on it. You don't need to track where you are on the belt, check whether items remain, or manually advance the belt—those mechanics are handled by the system. Your job is to focus entirely on what you do with each item.

SECTION 3

Visual Explanation — Compiler Expansion

The following diagram illustrates how the compiler transforms a range-based for loop into its equivalent iterator-based form. Understanding this expansion is crucial because it reveals what happens under the hood: the range expression is evaluated exactly once, begin() and end() are called to define the iteration boundaries, and the loop variable is initialized by dereferencing the iterator on each pass.

Range-Based For Loop → Compiler ExpansionSOURCE CODE (Range-Based)for (auto& x : vec) {std::cout << x;}COMPILER EXPANSION (Iterator){auto&& __range = vec;auto __begin = begin(__range);auto __end = end(__range);for (; __begin != __end; ++__begin) {auto& x = *__begin;std::cout << x;}}expands toStep-by-Step Execution (vec = {10, 20, 30})STEP 1__begin → [10]STEP 2x = 10, printSTEP 3++__begin → [20]STEP 4x = 20, print…continues until __begin == __end (past element 30), then the loop exits.The range expression (vec) is evaluated only once, before __begin and __end are set.
The left panel shows the concise range-based syntax; the right panel shows the equivalent code the compiler generates. The bottom section traces execution across the vector {10, 20, 30}, highlighting that the range expression is evaluated exactly once.

Several details in the expansion deserve attention. First, the range expression is bound to a forwarding reference (auto&&), which means both lvalues and rvalues are supported—you can write for (auto x : getVector()) without worrying about dangling references because the temporary is lifetime-extended for the duration of the loop. Second, __begin and __end may have different types (since C++17), enabling sentinel-based iteration as used in the Ranges library. Third, modifying the container's size during iteration (e.g., calling push_back) invalidates iterators and results in undefined behavior—exactly the same constraint as a hand-written iterator loop.

SECTION 4

How It Works — Declaration Modes & Type Deduction

The power and potential pitfalls of range-based for loops center on how the loop variable is declared. The declaration controls whether elements are copied, referenced, or const-referenced, and auto type deduction plays a central role in keeping the syntax concise. The following table formalizes the three primary declaration modes.

DeclarationEffectCopies?Modifies Original?
for (auto x : c)Each element is copied into xYes — every iterationNo — changes to x do not affect c
for (auto& x : c)x is a mutable reference to each elementNoYes — assignments to x modify c
for (const auto& x : c)x is a const reference to each elementNoNo — x is read-only
for (auto&& x : c)Universal reference — binds to lvalues and rvaluesNoDepends on context — useful for proxy iterators (e.g., std::vector<bool>)

Loop variable declaration modes and their semantic effects

The auto&& Subtlety

The universal reference form auto&& is occasionally recommended as a "generic" default because it correctly handles proxy iterator types such as those returned by std::vector<bool>. In a vector<bool>, dereferencing the iterator yields a temporary proxy object, not an actual bool&. Using auto& would fail to compile because you cannot bind a non-const lvalue reference to a temporary, whereas auto&& deduces correctly and extends the proxy's lifetime. For most standard containers, however, const auto& for read-only access and auto& for mutation remain the clearest and most idiomatic choices.

💡

Best Practice

Default to const auto& when you only need to read elements. This avoids unnecessary copies, expresses intent, and allows the compiler to make stronger optimizations. Switch to auto& only when you need to modify elements in place.

SECTION 5

Supported Containers & Custom Ranges

The range-based for loop works with any type that satisfies the range concept—loosely, any type for which free or member begin() and end() functions can be found via argument-dependent lookup (ADL). The standard library provides these for all STL containers, C-style arrays, and std::initializer_list. The following diagram classifies the major rangeable types.

Types Compatible with Range-Based For LoopsRangeable TypeC-Style ArraysSTL ContainersUser-Defined Typesint arr[] = {1,2,3};Compiler knows size atcompile timestd::initializer_listSequenceAssoc.Unorderedvectordequelistarraystringmapsetmultimapunordered_mapunordered_setAny class with:.begin() / .end()— or —Free functions via ADL:begin(obj) / end(obj)Associative Container Element Typesstd::map<K,V>→ Each element is std::pair<const K, V>for (auto& [key, val] : myMap)→ Structured binding (C++17)std::set<T> elements are always const — auto& deduces as const T&
A taxonomy of types compatible with range-based for loops. STL containers are subdivided into sequence, associative, and unordered categories. User-defined types need only provide begin() and end() functions. The bottom panel notes that map elements are pairs and set elements are implicitly const.

Making Your Own Type Rangeable

To use a range-based for loop with a custom class, you need to provide either member functions begin() and end() that return iterators, or free-standing functions begin(T&) and end(T&) discoverable via ADL. The iterators must support pre-increment (++it), dereference (*it), and inequality comparison (it != end). This minimal iterator protocol is easy to implement even for non-trivial data structures, making the range-based for loop extensible to domain-specific containers such as graph adjacency lists, file stream wrappers, or database result sets.

SECTION 6

Worked Example — Processing a Student Gradebook

Consider a program that reads a list of student scores, computes the class average, and then identifies all scores above that average. We will solve this step by step using range-based for loops on an std::vector<double> and an std::map<std::string, double>.

Gradebook Analysis with Range-Based For Loops

  1. 1

    Step 1 — Define the Data

    We create a std::map<std::string, double> that maps student names to their scores: std::map<std::string, double> grades = {{"Alice", 92.5}, {"Bob", 78.0}, {"Carol", 88.3}, {"Dave", 95.1}, {"Eve", 61.7}};
    A map with 5 entries, automatically sorted by key.
  2. 2

    Step 2 — Compute the Sum Using const auto&

    We iterate with const auto& because we only need to read values: double total = 0.0; for (const auto& [name, score] : grades) { total += score; } The structured binding [name, score] destructures each std::pair<const std::string, double> into its components.
    total = 415.6
  3. 3

    Step 3 — Compute the Average

    We divide total by the number of entries: double average = total / grades.size();
    average = 415.6 / 5 = 83.12
  4. 4

    Step 4 — Filter and Print Above-Average Scores

    A second range-based for loop compares each score against the average: for (const auto& [name, score] : grades) { if (score > average) { std::cout << name << ": " << score << '\n'; } }
    Output: Alice: 92.5, Carol: 88.3, Dave: 95.1
  5. 5

    Step 5 — Curve Scores In-Place Using auto&

    Now suppose we want to add 5 bonus points to every score. We switch to auto& to modify the map values in place: for (auto& [name, score] : grades) { score += 5.0; } Note that name is still const std::string& because map keys are always const.
    All scores are now increased by 5: Alice → 97.5, Bob → 83.0, Carol → 93.3, Dave → 100.1, Eve → 66.7

SECTION 7

Range-Based vs. Traditional Loops — Strengths & Limitations

Range-based for loops are not universally superior to traditional loops; rather, they excel in a specific and very common scenario—iterating over every element of a container. Understanding when to choose each style is an important aspect of writing clear, maintainable C++.

CriterionRange-Based ForIndex-Based ForIterator-Based For
ReadabilityExcellent — intent is immediately clearGood — familiar patternVerbose — type names clutter the code
Off-by-One RiskNone — boundaries are implicitModerate — < vs. <= errorsLow — but requires correct sentinel
Index AccessNot available nativelyDirect — the index is the loop variableRequires std::distance()
Partial IterationOnly via break or continueEasy — adjust start/end boundsEasy — set custom begin/end
Container GeneralityAll rangeable types (arrays, lists, maps, sets)Only random-access containersAll iterable containers
Element RemovalUnsafe — invalidates iteratorsPossible with index adjustmentSafe if using erase return value

Comparison of three C++ loop styles for container iteration

⚖️

WHEN TO USE EACH STYLE

Choose the range-based for loop as your default whenever you need to visit every element of a container and don't need the index. Think of it as the "90% case" tool—analogous to how a database query planner chooses a sequential scan for full-table operations. Reserve the index-based loop for algorithms that require positional information (e.g., computing pairwise differences between adjacent elements), and the iterator-based loop for algorithms that need to erase elements during iteration or operate on subranges.

SECTION 8

Connection to C++20 Ranges & Views

The range-based for loop laid the conceptual groundwork for C++20's Ranges library (<ranges>). While the basic range-based for loop iterates eagerly over an entire container, the Ranges library introduces lazy views that transform, filter, and compose ranges without creating intermediate containers. Importantly, these views are themselves rangeable—they work seamlessly with the for (auto& x : view) syntax.

FeatureRange-Based For (C++11)Ranges Library (C++20)
FilteringRequires if-statement inside the loop bodyviews::filter(pred) — filtering is part of the range definition
TransformationPerformed inside the loop body or via std::transform()views::transform(fn) — composable with pipe operator
Partial Iterationbreak statement or manual counterviews::take(n) — semantically expresses the intent
CompositionNesting loops or separate passes over the dataPipeline with | operator: v | filter(f) | transform(g) | take(5)
EvaluationEager — always visits elementsLazy — elements are computed on demand during iteration

Evolution from range-based for loops to the C++20 Ranges library

A concrete example demonstrates the power of composing views with range-based for. The expression for (auto x : numbers | std::views::filter([](int n){ return n % 2 == 0; }) | std::views::transform([](int n){ return n * n; })) iterates over only the squares of the even numbers in the container, without allocating any intermediate storage. The same range-based for syntax you already know serves as the consumption point for arbitrarily complex range pipelines, making the basic loop an essential building block for advanced C++ programming.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain the difference between declaring the loop variable as auto x versus const auto& x in a range-based for loop over a std::vector<std::string>. When would the performance difference matter, and why?
Show answer
Using auto x copies each std::string into the loop variable on every iteration, which involves a dynamic memory allocation for each string longer than the small-string optimization threshold. Using const auto& x creates a const reference to the original element, avoiding the copy entirely. The performance difference becomes significant when the vector contains many strings or strings with large contents, since each copy triggers a heap allocation. For trivially copyable types like int or double, the performance difference is negligible, and copying may even be preferred due to better cache behavior.

Question 2: BASIC

Write a range-based for loop that computes the product of all elements in a std::vector<int> nums = {2, 3, 5, 7};. What is the final product?
Show answer
The solution is: int product = 1; for (const auto& n : nums) { product *= n; }. After the loop, product equals 2 × 3 × 5 × 7 = 210. We initialize to 1 (the multiplicative identity) rather than 0 because multiplying by 0 would give 0 for any input. Using const auto& is appropriate since we only read each element. For primitive types like int, auto n (by value) would be equally efficient.

Question 3: INTERMEDIATE

Given std::map<std::string, std::vector<int>> studentScores; where each key is a student name and the value is a vector of their test scores, write nested range-based for loops using structured bindings to print each student's name followed by their average score.
Show answer
The solution uses an outer loop with a structured binding and an inner loop for summation: for (const auto& [name, scores] : studentScores) { double sum = 0.0; for (const auto& s : scores) { sum += s; } double avg = scores.empty() ? 0.0 : sum / scores.size(); std::cout << name << ": " << avg << '\n'; }. The outer loop destructures each map entry into name (a const std::string&) and scores (a const std::vector<int>&). The guard against empty vectors prevents division by zero. Note that both loops use const references, correctly expressing read-only intent.

Question 4: APPLIED

You are processing sensor readings stored in a std::vector<double> readings. Write a range-based for loop that clamps all values to the range [0.0, 100.0]—that is, any reading below 0 should be set to 0, and any reading above 100 should be set to 100. Then explain why you must use auto& instead of auto.
Show answer
The solution is: for (auto& r : readings) { if (r < 0.0) r = 0.0; else if (r > 100.0) r = 100.0; }. Alternatively, one could use r = std::clamp(r, 0.0, 100.0); from <algorithm> (C++17). We must use auto& (a mutable reference) because the assignment r = 0.0 needs to modify the original element inside the vector. If we declared auto r (by value), r would be a local copy, and assigning to it would have no effect on the vector's contents.

Question 5: CRITICAL THINKING

Consider the following code: std::vector<int> v = {1, 2, 3, 4, 5}; for (auto& x : v) { if (x == 3) { v.push_back(6); } } This code exhibits undefined behavior. Explain precisely why, by referring to the compiler expansion of the range-based for loop. Then propose two different safe alternatives that achieve the goal of appending 6 to the vector when a 3 is found.
Show answer
The compiler expands the range-based for loop by calling begin(v) and end(v) once before the loop begins, storing the resulting iterators. When push_back(6) is called, it may trigger a reallocation of the vector's internal buffer, invalidating all existing iterators—including the stored begin iterator and the loop's current position. Subsequent dereference or increment of the invalidated iterator is undefined behavior. Two safe alternatives: (1) Use a separate boolean flag during the loop and perform the push_back after the loop ends: bool found = false; for (const auto& x : v) { if (x == 3) found = true; } if (found) v.push_back(6); (2) Use an index-based loop, since indices remain valid after reallocation: size_t n = v.size(); for (size_t i = 0; i < n; ++i) { if (v[i] == 3) v.push_back(6); }. Note we capture n before the loop to prevent infinite growth.

SUMMARY

Summary — Range-Based For Loops

Summary

The range-based for loop, introduced in C++11, provides a concise syntax—for (declaration : range)—for iterating over every element of any rangeable type: C-style arrays, all STL containers, initializer lists, and user-defined types with begin()/end(). The compiler expands it into a traditional iterator-based loop where the range expression is evaluated once, and iterators are managed automatically—eliminating off-by-one errors and boilerplate.

The loop variable's declaration mode controls semantics: const auto& for read-only access (no copies), auto& for in-place mutation, and auto (by value) when an independent copy is needed. C++17 structured bindings extend this to associative containers via auto& [key, value]. The critical constraint is that modifying the container's size during iteration causes undefined behavior. The range-based for loop serves as the foundation for C++20's Ranges library, where lazy views compose with the same for (auto x : view) syntax.

Varsity Tutors • C++ • Range-Based For Loops — Use range-based for loops for containers