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

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

#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:

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:

{
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

#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

#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:

#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:

#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

#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

#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:

    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:

#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:

#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:

#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:

    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! :)