Skip to main content

C++ Range Based For Loop

Introduction

The range-based for loop is one of the most useful features introduced in C++11. It provides a simpler and more readable way to iterate through elements in a container (like arrays, vectors, maps) or any object that satisfies certain requirements for iteration.

Before C++11, iterating through a collection typically required:

  • Using indexed for loops with explicit counter variables
  • Using iterators with potentially complex syntax
  • Handling boundary conditions manually

The range-based for loop eliminates these complications by abstracting the iteration mechanics, allowing you to focus on what you want to do with each element.

Basic Syntax

cpp
for (element_declaration : collection) {
// Body of loop using element
}

Where:

  • element_declaration is the variable that will hold each element during iteration
  • collection is the sequence you want to iterate through

Simple Examples

Iterating Through an Array

cpp
#include <iostream>

int main() {
int numbers[] = {1, 2, 3, 4, 5};

// Range-based for loop
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

return 0;
}

Output:

1 2 3 4 5

Comparing with Traditional For Loop

Traditional for loop:

cpp
for (int i = 0; i < 5; i++) {
std::cout << numbers[i] << " ";
}

The range-based version is:

  • More concise
  • Eliminates off-by-one errors
  • Removes the need to know the size of the collection
  • Focuses on the element itself, not its position

How It Works

Under the hood, the range-based for loop is approximately equivalent to:

cpp
{
auto && __range = collection;
auto __begin = begin(__range);
auto __end = end(__range);
for (; __begin != __end; ++__begin) {
element_declaration = *__begin;
// loop body
}
}

This means the collection must provide either:

  1. Member functions begin() and end(), or
  2. Be usable with free functions begin() and end()

All standard containers and arrays automatically satisfy these requirements.

Using with STL Containers

Vector Example

cpp
#include <iostream>
#include <vector>

int main() {
std::vector<std::string> fruits = {"Apple", "Banana", "Cherry", "Date"};

for (const std::string& fruit : fruits) {
std::cout << "I like " << fruit << std::endl;
}

return 0;
}

Output:

I like Apple
I like Banana
I like Cherry
I like Date

Map Example

cpp
#include <iostream>
#include <map>

int main() {
std::map<std::string, int> ages = {
{"Alice", 25},
{"Bob", 30},
{"Charlie", 22}
};

for (const auto& pair : ages) {
std::cout << pair.first << " is " << pair.second << " years old." << std::endl;
}

return 0;
}

Output:

Alice is 25 years old.
Bob is 30 years old.
Charlie is 22 years old.

Modifying Elements While Iterating

To modify elements during iteration, use a reference:

cpp
#include <iostream>
#include <vector>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};

// Double each number in the vector
for (int& num : numbers) {
num *= 2;
}

// Print the modified values
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

return 0;
}

Output:

2 4 6 8 10

Reference vs. Value

There are three common ways to declare the loop variable:

  1. By value: for (int num : numbers)

    • Creates a copy of each element
    • Use when you don't need to modify the original collection
    • Best for small, inexpensive-to-copy types (int, char, etc.)
  2. By reference: for (int& num : numbers)

    • Provides direct access to each element
    • Use when you want to modify elements in the collection
    • Avoids copying large objects
  3. By const reference: for (const int& num : numbers)

    • Provides read-only access to each element
    • Use when you don't need to modify elements but want to avoid copying
    • Best practice for larger objects or when modification isn't needed

Type Inference with auto

You can use auto to let the compiler determine the element type:

cpp
#include <iostream>
#include <vector>

int main() {
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};

// Using auto
for (const auto& name : names) {
std::cout << "Hello, " << name << "!" << std::endl;
}

return 0;
}

Output:

Hello, Alice!
Hello, Bob!
Hello, Charlie!

This is especially useful for complex types or when using templates.

Real-World Applications

Processing a Collection of Data

cpp
#include <iostream>
#include <vector>
#include <string>

struct Student {
std::string name;
int grade;
};

double calculateAverageGrade(const std::vector<Student>& students) {
int sum = 0;

for (const auto& student : students) {
sum += student.grade;
}

return students.empty() ? 0 : static_cast<double>(sum) / students.size();
}

int main() {
std::vector<Student> classRoom = {
{"Alice", 92},
{"Bob", 85},
{"Charlie", 78},
{"Diana", 95}
};

std::cout << "Class average: " << calculateAverageGrade(classRoom) << std::endl;

// Find and print students with grades above 90
std::cout << "Honor students:" << std::endl;
for (const auto& student : classRoom) {
if (student.grade > 90) {
std::cout << "- " << student.name << " (" << student.grade << ")" << std::endl;
}
}

return 0;
}

Output:

Class average: 87.5
Honor students:
- Alice (92)
- Diana (95)

Filtering and Processing Data

cpp
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

int main() {
std::vector<std::string> messages = {
"Hello world",
"C++ programming",
"Range based loops",
"Modern C++ features"
};

// Convert all messages to uppercase
for (auto& message : messages) {
std::transform(message.begin(), message.end(), message.begin(), ::toupper);
}

// Print modified messages
for (const auto& message : messages) {
std::cout << message << std::endl;
}

return 0;
}

Output:

HELLO WORLD
C++ PROGRAMMING
RANGE BASED LOOPS
MODERN C++ FEATURES

Limitations and Considerations

  1. Cannot track indices: Range-based for doesn't provide the index of the current element. If you need indices, consider:

    cpp
    for (size_t i = 0; i < vec.size(); ++i) {
    // Use vec[i] and have index i
    }
  2. Cannot modify the container structure: Adding or removing elements during iteration may lead to undefined behavior.

  3. Works only with collections that have begin/end: Custom types need to properly implement these methods to work with range-based for.

Using with Custom Types

For your own classes to work with range-based for loops, you need to:

  1. Implement begin() and end() member functions or free functions, or
  2. Provide access to an iterable member

Example of a custom iterable class:

cpp
#include <iostream>
#include <vector>

class MyCollection {
private:
std::vector<int> data;

public:
MyCollection() : data{1, 2, 3, 4, 5} {}

// Provide begin() and end() methods
auto begin() { return data.begin(); }
auto end() { return data.end(); }

// Const versions for when the collection is const
auto begin() const { return data.begin(); }
auto end() const { return data.end(); }
};

int main() {
MyCollection collection;

for (int value : collection) {
std::cout << value << " ";
}
std::cout << std::endl;

return 0;
}

Output:

1 2 3 4 5

Compatibility with C++17 and C++20

Structured Bindings (C++17)

When combined with C++17's structured bindings, range-based for loops become even more powerful:

cpp
#include <iostream>
#include <map>

int main() {
std::map<std::string, int> scores = {
{"Alice", 95},
{"Bob", 87},
{"Charlie", 92}
};

// Using structured bindings
for (const auto& [name, score] : scores) {
std::cout << name << " scored " << score << " points." << std::endl;
}

return 0;
}

Output:

Alice scored 95 points.
Bob scored 87 points.
Charlie scored 92 points.

Ranges Library (C++20)

C++20 introduces the Ranges library, which expands on the range-based for loop concepts:

cpp
#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// Filter even numbers and print them
for (int n : numbers | std::views::filter([](int n) { return n % 2 == 0; })) {
std::cout << n << " ";
}
std::cout << std::endl;

return 0;
}

Output:

2 4 6 8 10

Summary

Range-based for loops are a modern C++ feature that simplifies iteration through collections. They provide:

  • More concise, readable code
  • Fewer potential bugs (like off-by-one errors)
  • Focus on what you want to do with each element, not the mechanics of iteration
  • Better performance potential through compiler optimizations

Best practices:

  • Use const references (const auto&) for most cases to avoid unnecessary copies
  • Use references (auto&) when you need to modify elements
  • Use value (auto) for small types like integers
  • Combine with auto for cleaner code
  • Use with structured bindings for key-value collections

Exercises

  1. Write a program that uses a range-based for loop to find the largest element in a vector of integers.

  2. Create a function that takes a vector of strings and returns a new vector containing only strings that start with a specific letter.

  3. Define a custom class that represents a deck of cards and implement the necessary methods so that you can use a range-based for loop to iterate through the cards.

  4. Write a program that uses a range-based for loop with a map to count the frequency of words in a string.

  5. Refactor the following code to use a range-based for loop:

    cpp
    std::vector<double> values = {1.1, 2.2, 3.3, 4.4, 5.5};
    double sum = 0;
    for (size_t i = 0; i < values.size(); ++i) {
    sum += values[i];
    }

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)