C++ Range-based For Loop
The range-based for loop (also called a “for-each” loop) is a simpler, safer way to iterate over every element of a container or array in C++, introduced in C++11. Instead of manually tracking an index or an iterator, you simply say “for each element in this collection, do something,” and the compiler handles the bookkeeping. It makes code shorter, harder to get wrong, and often faster to write, which is why it has become the default choice for iteration in modern C++.
Overview / How it works
Before C++11, looping over a container required either an index-based loop (for (int i = 0; i < v.size(); i++)) or an explicit iterator (for (auto it = v.begin(); it != v.end(); ++it)). Both work, but both are easy to get wrong: off-by-one errors, forgetting to increment, or dereferencing the wrong iterator are classic bugs. The range-based for loop removes that boilerplate by generating the iterator logic for you.
Under the hood, a range-based for loop is syntactic sugar. The compiler rewrites it into an equivalent loop that calls begin() and end() (either member functions on the container, or free functions found via argument-dependent lookup for things like C-style arrays), then repeatedly dereferences and advances an iterator until it reaches the end. This means the range-based for loop works with any type that exposes a compatible begin/end pair — std::vector, std::array, std::map, std::string, raw arrays, and even your own custom classes if you implement begin()/end().
You can iterate by value, by reference, or by const reference, and each has different performance and mutation implications:
- By value (
for (int n : nums)) copies each element into the loop variable. Cheap for small types likeint, but wasteful for large objects likestd::string, and any modification you make to the loop variable does not affect the original container. - By reference (
for (int &n : nums)) binds the loop variable directly to each element, so modifications do affect the original container, and no copy is made. - By const reference (
for (const int &n : nums)) avoids copying but prevents modification — the safest and most efficient choice when you only need to read each element.
Syntax
for (declaration : range_expression) {
// loop body, uses "declaration"
}
| Part | Meaning |
|---|---|
declaration |
The loop variable’s type and name, e.g. int n, auto& n, or const auto& n. auto lets the compiler deduce the element type. |
range_expression |
Any expression referring to something iterable: a container variable, an array, or a function call returning one. |
| loop body | Executed once per element, with declaration bound to the current element on each pass. |
Since C++17, you can also use structured bindings in the declaration to unpack elements like pairs or tuples directly, which is especially useful when iterating over std::map: for (const auto& [key, value] : myMap).
Examples
Example 1: Basic iteration by value
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {10, 20, 30, 40, 50};
for (int n : nums) {
cout << n << " ";
}
cout << endl;
return 0;
}
Output:
10 20 30 40 50
Here, n is a fresh int copy of each element in turn. Because int is small, copying is essentially free, so iterating by value is perfectly fine for read-only work on primitive types.
Example 2: Modifying elements with a reference
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3, 4, 5};
for (int &n : nums) {
n *= 2;
}
for (int n : nums) {
cout << n << " ";
}
cout << endl;
return 0;
}
Output:
2 4 6 8 10
The first loop uses int &n, binding n directly to each element of nums. Doubling n doubles the actual element in the vector. The second loop then confirms the change by printing the updated values.
Example 3: Structured bindings with std::map
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
map<string, int> scores = {{"Alice", 90}, {"Bob", 85}, {"Carol", 95}};
for (const auto &[name, score] : scores) {
cout << name << ": " << score << endl;
}
return 0;
}
Output:
Alice: 90
Bob: 85
Carol: 95
Each element of a std::map is a std::pair<const Key, Value>. The structured binding [name, score] unpacks that pair into two readable names in one step, instead of writing entry.first and entry.second. Note that std::map keeps its keys sorted, which is why the output appears alphabetically even though the elements were inserted in a different order.
How it works step by step
Conceptually, the compiler expands for (auto &x : container) { body } into something equivalent to:
auto &&__range = container;
auto __begin = begin(__range);
auto __end = end(__range);
for (; __begin != __end; ++__begin) {
auto &x = *__begin;
body
}
Walking through this: (1) the range expression is evaluated once and cached, so it’s not re-evaluated every iteration; (2) begin() and end() are called once to get the iteration boundaries; (3) the loop compares the current position to the end, dereferences it into your loop variable, runs the body, then advances the iterator. This is exactly what you’d write by hand with an iterator-based loop — the range-based for loop just writes it for you and reduces the surface area for mistakes.
Common Mistakes
Mistake 1: Expecting a by-value loop to modify the container
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3};
for (int n : nums) {
n = 0;
}
for (int n : nums) {
cout << n << " ";
}
cout << endl;
return 0;
}
Output:
1 2 3
This surprises many beginners: the values look unchanged even though the loop body assigns n = 0. That’s because int n is a copy of each element — setting n only changes the copy, not the vector. To actually zero out every element, use a reference:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3};
for (int &n : nums) {
n = 0;
}
for (int n : nums) {
cout << n << " ";
}
cout << endl;
return 0;
}
Output:
0 0 0
Adding & to the declaration makes n an alias for the real element, so the assignment now updates the vector itself.
Mistake 2: Changing the container’s size while iterating over it
Adding or removing elements from a container (for example, calling push_back or erase on a vector) while a range-based for loop is running over it invalidates the cached begin/end iterators. The loop has no idea the container changed underneath it, so this leads to undefined behavior — it might crash, loop forever, or silently corrupt data, depending on the compiler and standard library implementation:
vector<int> v = {1, 2, 3};
for (int x : v) {
v.push_back(x); // undefined behavior: invalidates the range's iterators
}
If you need to grow or shrink a container based on its own contents, iterate over a copy, build a separate result container, or use an index-based/iterator-based loop that explicitly accounts for the resizing.
Best Practices
- Default to
const auto&when you only need to read elements — it avoids copies and prevents accidental modification. - Use
auto&(non-const reference) only when you intend to modify the elements in place. - Reserve iteration by value (
auto) for small, cheap-to-copy types likeint,char, ordouble, or when you explicitly want a local copy to modify without touching the original. - Never resize, insert into, or erase from the container you’re iterating over inside the loop body.
- Use structured bindings (
const auto& [k, v]) when iterating overstd::map,std::pair, or other tuple-like elements — it’s far more readable than.first/.second. - Prefer the range-based for loop over index-based loops whenever you don’t need the index itself; it eliminates off-by-one and iterator-invalidation-style bugs by construction.
- If you need both the index and the element, a traditional indexed loop (or
std::views::enumeratein C++23) is clearer than trying to force an index into a range-based for loop.
Practice Exercises
- Exercise 1: Given
vector<double> prices = {19.99, 5.50, 42.00, 3.25};, write a range-based for loop that prints each price formatted as$19.99, etc. - Exercise 2: Given
vector<int> nums = {4, 15, 8, 23, 42, 16};, use a range-based for loop with a reference to increment every element by 10, then print the resulting vector. Expected output:14 25 18 33 52 26. - Exercise 3: Given
map<string, int> inventory = {{"apples", 12}, {"bananas", 7}, {"cherries", 30}};, use a range-based for loop with structured bindings to print each item and quantity, and calculate the total quantity across all items.
Summary
- The range-based for loop iterates over every element of a container or array without manual index or iterator management.
- It’s syntactic sugar that the compiler expands into a
begin()/end()-based iterator loop. - Iterating by value copies elements and cannot modify the original container.
- Iterating by reference (
&) allows in-place modification with no copying. - Iterating by const reference (
const auto&) is the safest, most efficient default for read-only access. - Structured bindings (
[key, value]) make iterating over maps and pairs much cleaner. - Never modify a container’s size while range-for-looping over it — it causes undefined behavior.
