Range-Based for Loops
Reviewed & published by Brayan K
Most of the time when you loop over a collection, you don't actually care about the index — you just want to touch every element. The range-based for loop says exactly that, with far less to get wrong: no i < size(), no ++i, no off-by-one bugs. By the end of this lesson you'll iterate vectors, strings, and maps cleanly, and know precisely when to use a reference.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Write for (auto x : container) to visit every element
- Choose value, auto&, and const auto& correctly
- Iterate vectors, strings, arrays, and initializer lists
- Loop over a map with structured bindings [key, value]
- Avoid resizing a container while looping over it
- Know when a classic indexed loop is still the right tool
💡 Real-World Analogy
A classic indexed loop is like reading a book by page number: "go to page 1, read it, go to page 2…" — you have to track the page count and not run past the end. A range-based loop is like flipping through the book page by page: you just say "for each page, read it", and the book takes care of where it starts and stops. You give up knowing the page number, but you gain simplicity and you can never fall off the end.
📊 Loop Variable Cheat Sheet
| Form | Behaviour | Use when |
|---|---|---|
| for (auto x : c) | copy of each element | cheap types (int), read-only |
| for (auto& x : c) | reference (can modify) | you want to change elements |
| for (const auto& x : c) | read-only reference | read big objects, no copy |
| for (auto&& x : c) | forwarding reference | generic / advanced code |
Rule of thumb: const auto& to read, auto& to write, plain auto only for tiny types.
1. The Basic Range-Based for
The syntax is for (Type element : container). On each pass, element takes the value of the next item in the container, and the loop ends automatically when there are no more. There's no index, no bounds check, no increment to write — which removes a whole category of off-by-one bugs. Compare it with the equivalent index-based loop in the example.
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> scores = {90, 75, 88, 100};
// Range-based for: visit each element directly, no index needed.
// for (Type element : container) { ... }
for (int s : scores) {
cout << s << " "; // 90 75 88 100
}
cout << endl;
// Compare with the OLD index-based loop -- more to get wrong:
for (size_t i = 0; i < scores.size(); ++i) {
cout << scores[i] << " "; // same output, more boilerplate
}
cout << endl;
return 0;
}
// ✅ Expected output:
// 90 75 88 100
// 90 75 88 1002. Value vs Reference — the Key Decision
The one thing you must get right is the loop variable's form. A plain value (int n or auto n) iterates over copies, so writing to it doesn't change the container. To modify elements, use auto&. To read large objects (like strings) efficiently without copying, use const auto&. This single choice is the most common source of "my loop didn't change anything" confusion.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3, 4};
// Plain value -> works on a COPY. The container is unchanged.
for (int n : nums) n *= 10;
cout << nums[0] << endl; // 1 (unchanged)
// auto& -> a REFERENCE, so changes modify the real elements.
for (auto& n : nums) n *= 10;
cout << nums[0] << endl; // 10
// const auto& -> read-only AND avoids copying big objects.
vector<string> words = {"apple", "banana", "cherry"};
for (const auto& w : words) {
cout << w << " (" << w.size() << ") ";
}
cout << endl; // apple (5) banana (6) cherry (6)
return 0;
}
// ✅ Expected output:
// 1
// 10
// apple (5) banana (6) cherry (6)Your turn. The loop should add 5 to every score and have it stick. Fill in the blank:
#include <iostream>
#include <vector>
using namespace std;
int main() {
// 🎯 YOUR TURN — add 5 bonus points to every score IN PLACE.
vector<int> scores = {70, 80, 90};
// Fill in the blank so the change actually sticks:
for (___ s : scores) { // 👉 auto&
s += 5;
}
for (int s : scores) cout << s << " ";
cout << endl;
// ✅ Expected output: 75 85 95
return 0;
}🧩 Reorder Challenge
These lines sum a vector with a range-based loop. Put them in the right order:
A. for (int n : v) sum += n;
B. int sum = 0;
C. vector<int> v = {1, 2, 3, 4};
D. cout << sum << endl;
E. int main() {
F. return 0;
G. }Correct order: E → C → B → A → D → F → G
Open main (E), declare the vector (C), initialize sum to 0 (B), accumulate over every element (A), print the result 10 (D), then return 0; (F) and close the brace (G). sum must exist before the loop adds to it.
3. Strings, Maps & Structured Bindings
Anything with begin() and end() is iterable, so the range-based loop works on far more than vectors. A std::string is a range of char. A std::map yields key/value pairs, which you can unpack directly in the loop header with structured bindings: for (const auto& [key, value] : myMap). This is the cleanest way to walk a map.
#include <iostream>
#include <string>
#include <map>
using namespace std;
int main() {
// A std::string is a range of chars:
string word = "Hello";
int vowels = 0;
for (char c : word) {
if (c=='a'||c=='e'||c=='i'||c=='o'||c=='u'||
c=='A'||c=='E'||c=='I'||c=='O'||c=='U') ++vowels;
}
cout << "vowels: " << vowels << endl; // vowels: 2
// A std::map yields key/value pairs -> unpack with [key, value]:
map<string,int> ages = {{"Al", 30}, {"Bo", 25}};
for (const auto& [name, age] : ages) { // structured bindings
cout << name << " is " << age << endl;
}
// Al is 30
// Bo is 25
return 0;
}
// ✅ Expected output:
// vowels: 2
// Al is 30
// Bo is 254. Arrays & Initializer Lists
You can range directly over a braced list like {2, 4, 6, 8} and over plain C-style arrays — a built-in array knows its own length, so no sizeof arithmetic is needed. This makes quick aggregations (sum, max, count) genuinely tidy.
#include <iostream>
using namespace std;
int main() {
// You can range over a braced initializer list directly:
int total = 0;
for (int x : {2, 4, 6, 8}) total += x;
cout << "sum: " << total << endl; // sum: 20
// ...and over a plain C-style array (it knows its own length):
double temps[] = {19.5, 21.0, 23.5};
double hottest = temps[0];
for (double t : temps) {
if (t > hottest) hottest = t;
}
cout << "hottest: " << hottest << endl; // hottest: 23.5
return 0;
}
// ✅ Expected output:
// sum: 20
// hottest: 23.5🔎 Deep Dive: what the loop expands to
A range-based loop is "syntactic sugar". The compiler rewrites for (auto x : c) into roughly the iterator loop below — which is exactly why it works on anything with begin() and end():
{
auto&& __range = c;
auto __it = begin(__range);
auto __end = end(__range);
for (; __it != __end; ++__it) {
auto x = *__it; // your loop body uses x
// ...
}
}This also explains the danger of resizing inside the loop: operations like push_back can reallocate a vector and invalidate __it and __end, leading to undefined behaviour. If you must add or remove elements, use a classic loop or build a new container.
Pro Tips
- 💡 Default to const auto&: it reads any element with no copy; reach for auto& only when you modify.
- 💡 Use structured bindings on maps: [key, value] beats .first/.second every time.
- 💡 Need the index? Don't force it: use a separate counter or a classic loop instead of contorting the range loop.
- 💡 Never resize while ranging: collect changes and apply them after the loop.
🧠 Quick Recall
Predict the output before revealing the answer.
vector<int> v = {1, 2, 3};
for (int x : v) x += 10;
for (int x : v) cout << x << " ";1 2 3 — the first loop modifies copies, not the vector. You'd need auto& for the change to stick.
2. What does this print?
int sum = 0;
for (int x : {5, 10, 15}) sum += x;
cout << sum;30 — you can range over an initializer list directly: 5 + 10 + 15 = 30.
3. How many characters does this loop visit?
int n = 0;
for (char c : string("C++")) n++;
cout << n;3 — the string "C++" has three characters: 'C', '+', '+'.
Common Errors (and the fix)
- Loop "doesn't change anything": you used a plain value. Switch to for (auto& x : c) to modify the container.
- Silent copies of big objects: for (string s : words) copies every string. Use const auto&.
- Resizing while iterating: calling push_back / erase during the loop is undefined behaviour. Apply changes afterward.
- Wanting the index: the range loop doesn't expose one. Keep a counter or use a classic for.
- Ranging over a temporary's member: for (auto& x : makeVector().items) can dangle. Bind the container to a named variable first.
📋 Quick Reference
| Task | Code |
|---|---|
| Read each element | for (const auto& x : c) |
| Modify each element | for (auto& x : c) |
| Loop a string | for (char ch : word) |
| Loop a map | for (auto& [k, v] : m) |
| Loop a list | for (int x : {1,2,3}) |
| Loop an array | for (double t : temps) |
Mini-Challenge: Shopping Cart Report
One pass, two results: total all prices and count the pricey ones — all with a single range-based loop.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main() {
// 🎯 MINI-CHALLENGE: Shopping cart report
// Given the prices below:
// 1. Use a range-based for with const auto& to sum all prices.
// 2. Count how many items cost more than 10.0.
// 3. Print the total and the count of pricey items.
//
vector<double> prices = {4.50, 12.00, 8.25, 20.00, 15.50};
//
// ✅ Example output:
// total: 60.25
// pricey items: 3
// your code here
return 0;
}🎉 Lesson Complete
- ✅ for (auto x : container) visits every element — no index, no off-by-one
- ✅ Plain value copies; auto& modifies; const auto& reads big objects cheaply
- ✅ Works on vectors, strings, arrays, and braced initializer lists
- ✅ Iterate maps with structured bindings: for (auto& [k, v] : m)
- ✅ Never resize a container while ranging over it
Practice quiz
What is the syntax of a range-based for loop?
- for (Type element : container)
- for (int i = 0; i < n; ++i)
- for each (element in container)
- foreach (container as element)
Answer: for (Type element : container). The form is for (Type element : container), which visits each element directly with no index.
What does this print? vector<int> v = {1,2,3}; for (int x : v) x += 10; for (int x : v) cout << x << ' ';
- 11 12 13
- 1 2 3
- 10 20 30
- nothing
Answer: 1 2 3. A plain value loop variable iterates over copies, so x += 10 doesn't change the vector. It prints 1 2 3.
Which loop form lets you modify each element of a container in place?
- for (auto x : c)
- for (const auto& x : c)
- for (auto& x : c)
- for (int x : c)
Answer: for (auto& x : c). auto& binds a reference to each element, so writing to it changes the real container element.
What prints? int sum = 0; for (int x : {5, 10, 15}) sum += x; cout << sum;
- 0
- 15
- 30
- It does not compile
Answer: 30. You can range over a braced initializer list directly: 5 + 10 + 15 = 30.
How do you cleanly iterate a std::map in a range-based for loop?
- for (const auto& [key, value] : myMap)
- for (auto pair : myMap.keys())
- for (int i : myMap)
- for (key, value in myMap)
Answer: for (const auto& [key, value] : myMap). A map yields key/value pairs; structured bindings [key, value] unpack each pair in the loop header.
Is it safe to call push_back or erase on a container while ranging over it with a range-based for?
- Yes, the loop adjusts automatically
- No, resizing can invalidate the internal iterators and cause undefined behavior
- Yes, but only for std::vector
- Only if you also keep an index
Answer: No, resizing can invalidate the internal iterators and cause undefined behavior. Operations that resize the container can invalidate begin()/end(), leading to undefined behavior. Apply changes after the loop.
Can you get the current index inside a range-based for loop directly?
- Yes, via the built-in index variable
- No — you keep a separate counter or use a classic indexed loop
- Yes, by calling element.index()
- Only on std::array
Answer: No — you keep a separate counter or use a classic indexed loop. The range-based loop does not expose an index; if you need one, keep a counter or use a classic for loop.
How many characters does this loop visit? int n = 0; for (char c : string("C++")) n++; cout << n;
- 2
- 3
- 4
- 1
Answer: 3. A std::string is a range of chars, and "C++" has three characters: 'C', '+', '+'.
Why prefer for (const auto& s : words) over for (string s : words) for a vector of strings?
- It sorts the strings first
- It avoids copying each string while reading them
- It makes the loop run in parallel
- It allows modifying each string
Answer: It avoids copying each string while reading them. const auto& binds a reference, so no copy of each (potentially large) string is made; plain value copies every one.
Which of these can a range-based for loop iterate over?
- Only std::vector
- Only containers with an index operator
- Anything with begin() and end(), including arrays and initializer lists
- Only std::map and std::set
Answer: Anything with begin() and end(), including arrays and initializer lists. The loop is sugar over begin()/end(), so it works on vectors, strings, C-arrays, maps, and braced lists.
Continue this course
- Previous: auto & decltype
- Next: Functions — Write reusable functions with parameters, return types, and overloading
- Quick reference: C++ cheat sheet › Modern C++
Frequently asked questions
What is a range-based for loop?
It is a loop that visits every element of a container directly, without you managing an index or an iterator. The form is for (Type element : container) { ... }. It works on arrays, std::vector, std::string, std::map, and anything with begin() and end().
When should I use a reference in the loop variable?
Use for (auto& x : c) when you need to modify the elements. Use for (const auto& x : c) when you only read them — it avoids copying large objects. Use a plain value for (auto x : c) only for cheap-to-copy types like int when you don't need to change the container.
Can I get the index in a range-based for loop?
Not directly — that is the trade-off. If you need the index, keep a separate counter, use std::views::enumerate in C++23, or fall back to a classic indexed for loop. Range-based loops are about clarity when the index is not needed.
Is it safe to add or remove elements while ranging over a container?
No. Modifying a container's size (push_back, erase, insert) during a range-based for loop can invalidate the internal iterators and cause undefined behaviour. Collect changes separately, or use a classic loop that handles invalidation explicitly.
Does the range-based for loop work with std::map?
Yes. Each element is a key/value pair, so you can write for (const auto& [key, value] : myMap) using structured bindings, which is the clean modern way to iterate a map.