Embercode
✕
C++ LoopsLesson 9 of 16

40 min read

Range-for and for-each

Why this matters

Most loops in real programs visit every item in a list. A class register has every student. A shopping cart has every item. A list of daily temperatures has every day. The counting for loop can do this, but it asks you to start a counter, check it and step it correctly. Each of those steps is a chance for a bug. A range-based for loop, also called range-for, removes the counter when you do not need it, so the loop says what it does in one line.

This lesson covers range-for and the for-each style it supports. It also covers the cases where you still need the index, the position of an item in the list. The counting loops with steps and reversed ranges are in another lesson of this module, and the while loop is in the first lesson.

The problem with an index

Suppose you have four scores and want their total. A counting loop looks like this:

for (std::size_t i = 0; i < scores.size(); ++i) {
    total += scores[i];
}

Here i is the index, the position of an item, counted from 0. The first item is at index 0, the second at index 1, and so on. scores[i] reads the item at that position. scores.size() is the number of items.

The loop has three rules that must all be right:

  • start at 0, not 1;
  • stop when i reaches the size, using <, not <=;
  • read scores[i], not scores[i + 1].

Each rule is easy alone. Together they are where off-by-one errors come from. An off-by-one error is a loop that runs one time too many or one time too few. If you write <= instead of <, the loop tries to read scores[4] from a vector with four items. Index 4 does not exist, so the value you read is not one of your scores. C++ does not check [] for you, so the program may print a wrong number or crash.

The range-for version of the same total is:

for (int score : scores) {
    total += score;
}

Read it as "for each score in scores". The loop decides where to start and where to stop. There is no counter to set wrongly.

The idea

The general shape is:

for (declaration : range) {
    body
}

The parts have plain names:

  • The range is the thing you loop over. In this lesson it is a std::vector, which is a list that can grow. C-style arrays, std::string, std::map and std::set also work. A range is anything that has a beginning and an end, which the compiler finds for you.
  • The declaration names a variable that holds one element on each pass. Its type is the type of the element, or auto to let the compiler work out the type.
  • The body runs once for each element, from the first to the last.

The number of passes equals the number of elements. You never write that number down, so you cannot get it wrong.

Two words you will see in this module. An element is one item in the range. An iteration is one pass of the loop, which handles one element.

Example 1: adding up a list

Here is a complete program. It adds four scores and prints the total, the count and the average.

#include <iostream>
#include <vector>
 
int main() {
    std::vector<int> scores = {72, 85, 90, 64};
 
    int total = 0;
    for (int score : scores) {
        total += score;
    }
 
    std::cout << "total: " << total << '\n';
    std::cout << "count: " << scores.size() << '\n';
    std::cout << "average: " << total / static_cast<int>(scores.size()) << '\n';
}

Compile and run it:

g++ -std=c++20 -Wall -o range1 range1.cpp
./range1

The output is:

total: 311
count: 4
average: 77

Walk through it. total starts at 0. On the first pass, score holds 72 and total += score makes total 72. On the second pass score holds 85, and so on. After the fourth pass the loop ends because there are no more elements.

The average is 77, not 77.75. Both total and the count are whole numbers, and C++ division between two whole numbers drops the fraction. The static_cast<int> turns scores.size() into an ordinary int so the division uses the same kind of number. Decimal numbers are covered in the floating-point loop drift lesson.

Example 2: copy, reference and read-only reference

The declaration decides what the loop variable is. There are three common forms.

  • int score (or auto score) makes a copy. The loop gets its own value. Changing it does nothing to the vector.
  • auto& x makes a reference. A reference is another name for the same element. Changing x changes the vector.
  • const auto& name makes a read-only reference. You can look at the element, but you cannot change it. This is the usual choice for big items such as strings, because it avoids copying each one.

Here is a program that shows the first two forms:

#include <iostream>
#include <string>
#include <vector>
 
int main() {
    std::vector<int> values = {1, 2, 3};
 
    // Copy each element into x. Changing x changes only the copy.
    for (int x : values) {
        x *= 2;
    }
    std::cout << "after copy loop:";
    for (const auto& v : values) {
        std::cout << ' ' << v;
    }
    std::cout << '\n';
 
    // Refer to each element. Changing x changes the vector.
    for (auto& x : values) {
        x *= 2;
    }
    std::cout << "after reference loop:";
    for (const auto& v : values) {
        std::cout << ' ' << v;
    }
    std::cout << '\n';
 
    std::vector<std::string> names = {"Asha", "Ravi", "Meena"};
    for (const auto& name : names) {
        std::cout << "Hello, " << name << '\n';
    }
}

Output:

after copy loop: 1 2 3
after reference loop: 2 4 6
Hello, Asha
Hello, Ravi
Hello, Meena

The first loop doubles each copy and throws the copies away. The vector still holds 1, 2 and 3. The second loop doubles the elements themselves, so the vector holds 2, 4 and 6 afterwards.

A simple rule covers most cases:

  • Use int or double when you only read a small value. Copying a number costs almost nothing.
  • Use const auto& when you only read a bigger item, such as a std::string.
  • Use auto& when the loop must change the elements.

Example 3: a loop over a map

A std::map stores pairs. Each pair has a key, such as a name, and a value, such as a mark. A structured binding lets you unpack a pair into two named variables in the loop header.

#include <iostream>
#include <map>
#include <string>
 
int main() {
    std::map<std::string, int> marks = {{"Asha", 88}, {"Ravi", 73}};
    for (const auto& [name, mark] : marks) {
        std::cout << name << ": " << mark << '\n';
    }
}

Output:

Asha: 88
Ravi: 73

A std::map keeps its keys in sorted order, so the names come out alphabetically. You did not ask for that sorting. It is a property of the map, and a range-for loop walks the map in the order the map holds its items.

When you still need the index

Range-for gives you the element, not the position. Keep a counting loop when the position is part of the answer. These are the common cases:

  1. You must report where something is, such as the position of the largest value.
  2. You must compare an item with its neighbour.
  3. You must print a numbered list.
  4. You have two lists of the same length, and item i in one matches item i in the other.

Example 4: where the index matters

This program covers the first three cases on one list of scores and tasks.

#include <cstddef>
#include <iostream>
#include <map>
#include <ranges>
#include <string>
#include <vector>
 
int main() {
    std::vector<int> scores = {72, 85, 90, 64};
 
    // Position of the largest score: the index is needed for the answer.
    std::size_t best = 0;
    for (std::size_t i = 1; i < scores.size(); ++i) {
        if (scores[i] > scores[best]) {
            best = i;
        }
    }
    std::cout << "best is at index " << best << " with " << scores[best] << '\n';
 
    // Compare each score with the next one.
    for (std::size_t i = 0; i + 1 < scores.size(); ++i) {
        std::cout << scores[i] << " -> " << scores[i + 1] << '\n';
    }
 
    // Print a numbered list.
    std::vector<std::string> tasks = {"wash", "cook", "read"};
    for (std::size_t i = 0; i < tasks.size(); ++i) {
        std::cout << i + 1 << ". " << tasks[i] << '\n';
    }
 
    // Key and value pairs from a map, without an index.
    std::map<std::string, int> marks = {{"Asha", 88}, {"Ravi", 73}};
    for (const auto& [name, mark] : marks) {
        std::cout << name << ": " << mark << '\n';
    }
 
    // Reverse order without an index, using a view from <ranges>.
    for (int s : scores | std::views::reverse) {
        std::cout << s << ' ';
    }
    std::cout << '\n';
}

Output:

best is at index 2 with 90
72 -> 85
85 -> 90
90 -> 64
1. wash
2. cook
3. read
Asha: 88
Ravi: 73
64 90 85 72 

The first loop keeps best, the index of the best score seen so far. It starts at index 0, so the first comparison happens at index 1. Each time it finds a larger score, it stores that position. The loop ends with the position of 90, which is index 2.

The second loop compares each score with the one after it. It stops when i + 1 reaches the size, so scores[i + 1] is always a real item. The test is written as i + 1 < scores.size() and not i < scores.size() - 1. The reason is that size() returns an unsigned number. On an empty vector, 0 - 1 does not give -1. It wraps around to a very large number, and the loop would run past the end. Writing the test as i + 1 < scores.size() avoids that.

The third loop prints i + 1 so the list starts at 1, which is how people count. The index stays 0-based inside the program, and only the printed number is shifted.

The fourth loop walks the map without an index, as in example 3.

The last loop uses std::views::reverse. A view is a light wrapper that changes how you see a range without copying it. The | symbol passes the scores through the view. The header <ranges> must be included, or the compiler reports that std::views has not been declared. The reverse view is a C++20 feature, which this module uses.

Some newer C++ versions also offer a view that gives the index and the element together. This module compiles with -std=c++20, and the compiler rejects that view there:

error: ‘enumerate’ is not a member of ‘std::views’

Use an index loop for that job in C++20, as the first three loops in example 4 do.

Why the index type is std::size_t

The size() function returns a type called std::size_t, which is defined in the <cstddef> header. It is an unsigned type. An unsigned type holds zero and positive numbers only. Using the same type for the index keeps the comparison with size() clean. The mistakes section shows what happens when the types do not match.

Common mistakes

1. Expecting a copy to change the vector

This loop compiles without any warning, and it changes nothing:

for (int x : values) {
    x *= 2;
}

The loop variable x is a copy, so the vector keeps its old values. The fix is to declare the variable as a reference: for (auto& x : values). The program in example 2 shows both outcomes.

2. Comparing an int index with size()

This program is common in beginner code:

#include <iostream>
#include <vector>
 
int main() {
    std::vector<int> v = {10, 20, 30};
 
    for (int i = 0; i < v.size(); ++i) {
        std::cout << v[i] << '\n';
    }
}

Compiled with -Wall, GCC 15 prints this warning:

wsign.cpp: In function ‘int main()’:
wsign.cpp:7:23: warning: comparison of integer expressions of different signedness: ‘int’ and ‘std::vector<int>::size_type’ {aka ‘long unsigned int’} [-Wsign-compare]
    7 |     for (int i = 0; i < v.size(); ++i) {
      |                     ~~^~~~~~~~~~

The message means the loop compares a signed int with an unsigned size_type. Here the loop still prints 10, 20 and 30, but the comparison is not safe in general. A negative int becomes a very large unsigned number when it is compared, so i < v.size() can give the wrong answer. The fix is to write std::size_t i = 0, as the examples in this lesson do.

3. Changing the size of the vector inside the loop

This code adds an item while a range-for loop walks the vector:

#include <iostream>
#include <vector>
 
int main() {
    std::vector<int> v = {10, 20, 30};
 
    for (int x : v) {
        if (x == 20) {
            v.push_back(40);
        }
    }
    std::cout << "size: " << v.size() << '\n';
}

Without a checking tool, this program may print size: 4 and look fine. That result is luck. A push_back can move all the items to a new block of memory when the old block is full. The loop still points into the old block, which has been released. Reading a released block is a bug, and the result can change from run to run.

Compiling with AddressSanitizer, a tool that checks memory use while the program runs, shows the bug:

g++ -std=c++20 -Wall -g -fsanitize=address -o lesson_range_bug_a lesson_range_bug.cpp
./lesson_range_bug_a

The first lines of the report, with the path and the long addresses shortened, are:

==116265==ERROR: AddressSanitizer: heap-use-after-free on address 0x74347b5e0018 at pc 0x603f54bc2666 bp 0x7ffc811dfab0 sp 0x7ffc811dfaa0
READ of size 4 at 0x74347b5e0018 thread T0
    #0 0x603f54bc2665 in main .../lesson_range_bug.cpp:7

The addresses and the process number change on each run. The useful part is "heap-use-after-free", which means the program read memory it had already released. The line number, 7, is the range-for line. The rule is simple: do not change the size of a vector while a range-for loop walks it. Build a second vector with the new items, and add them after the loop ends.

4. Passing a pointer where a range is needed

A pointer to an array does not know where the array ends, so it is not a range:

#include <iostream>
 
int main() {
    int arr[] = {1, 2, 3};
    int* p = arr;
    for (int x : p) {
        std::cout << x << '\n';
    }
}

GCC 15 rejects it with two errors:

range5.cpp:6:18: error: ‘begin’ was not declared in this scope
range5.cpp:6:18: error: ‘end’ was not declared in this scope

The message names begin and end because a range must provide a beginning and an end. The fix is to loop over the array itself, which is a range. This version compiles and prints 1, 2 and 3 on separate lines:

int arr[] = {1, 2, 3};
for (int x : arr) {
    std::cout << x << '\n';
}

If you only have a pointer and a size, use a counting loop with the size.

5. Copying large items by accident

A declaration such as for (auto name : names) copies each std::string in the list. The output is correct, but the program does more work than it needs to. Write const auto& name when you only read the item.

Practice

Exercise 1

Count how many days in a list of temperatures are above 30. Use range-for, with no index.

std::vector<int> temps = {28, 31, 35, 30, 33};

Show the answer

The program adds 1 to a counter for each value that is greater than 30. The output is 3, because 31, 35 and 33 pass the test and 30 does not.

#include <iostream>
#include <vector>
 
int main() {
    std::vector<int> temps = {28, 31, 35, 30, 33};
    int hot = 0;
    for (int t : temps) {
        if (t > 30) {
            hot += 1;
        }
    }
    std::cout << "hot days: " << hot << '\n';
}

Output:

hot days: 3

Exercise 2

Find the longest word in a list. Use range-for, with no index. If two words have the same length, keep the first one.

std::vector<std::string> words = {"cat", "elephant", "dog", "giraffe"};

Show the answer

The loop replaces longest only when a word is strictly longer, so the first word of the longest length stays.

#include <iostream>
#include <string>
#include <vector>
 
int main() {
    std::vector<std::string> words = {"cat", "elephant", "dog", "giraffe"};
    std::string longest;
    for (const auto& w : words) {
        if (w.size() > longest.size()) {
            longest = w;
        }
    }
    std::cout << "longest: " << longest << '\n';
}

Output:

longest: elephant

Exercise 3

Print each even value in a vector together with its position in the vector. The position is part of the output, so you need an index.

std::vector<int> v = {5, 8, 3, 12, 7, 10};

Show the answer

Use a counting loop with std::size_t. The position is i, and the value is v[i].

#include <cstddef>
#include <iostream>
#include <vector>
 
int main() {
    std::vector<int> v = {5, 8, 3, 12, 7, 10};
    for (std::size_t i = 0; i < v.size(); ++i) {
        if (v[i] % 2 == 0) {
            std::cout << "index " << i << ": " << v[i] << '\n';
        }
    }
}

Output:

index 1: 8
index 3: 12
index 5: 10

Exercise 4

This loop is meant to multiply every value by 10, but it does not change the vector. Find the problem and fix it. Show the vector after the fix.

std::vector<int> v = {1, 2, 3};
for (int x : v) {
    x = x * 10;
}

Show the answer

The variable x is a copy. The fix is to make it a reference, so each assignment changes the element in the vector.

#include <iostream>
#include <vector>
 
int main() {
    std::vector<int> v = {1, 2, 3};
    for (auto& x : v) {
        x = x * 10;
    }
    for (int x : v) {
        std::cout << x << '\n';
    }
}

Output:

10
20
30

Exercise 5

Two vectors hold names and scores, and the scores match the names by position. Print one line for each person, such as Asha scored 88.

std::vector<std::string> names = {"Asha", "Ravi", "Meena"};
std::vector<int> scores = {88, 73, 91};

Show the answer

Each name and score match by position, so the same index i reads both vectors. The loop uses std::size_t and the size of names.

#include <cstddef>
#include <iostream>
#include <string>
#include <vector>
 
int main() {
    std::vector<std::string> names = {"Asha", "Ravi", "Meena"};
    std::vector<int> scores = {88, 73, 91};
    for (std::size_t i = 0; i < names.size(); ++i) {
        std::cout << names[i] << " scored " << scores[i] << '\n';
    }
}

Output:

Asha scored 88
Ravi scored 73
Meena scored 91

Summary

  • A range-for loop visits every element in order, and it needs no counter.
  • The loop variable is a copy unless you write &, so use auto& to change elements.
  • Use const auto& to read bigger items such as strings without copying them.
  • Keep a counting loop with std::size_t when the position is part of the answer.
  • Do not change the size of a vector while a range-for loop walks it.
  • A pointer is not a range, so loop over the array or the vector itself.
Practice this topic: coding problems for C++ Loops →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Step and reverse ranges