Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

C++11 Range-Based For Loops

Introduction

The range-based for loop, introduced in C++11, provides a simpler, more readable syntax for iterating over elements of a range, such as arrays, standard library containers, and custom types that satisfy the necessary requirements.

Basic Syntax

for (declaration : expression) {
    // loop statement(s)
}

Where declaration is type variable:

for (type variable : expression) {
    // loop statement(s)
}
  • type: The type of the elements (can be explicit like int, std::string, or use auto)
  • variable: The name of the variable that will hold each element
  • declaration: The complete variable declaration (type variable), whose type must be compatible with the element type of the sequence. The auto keyword is highly recommended here.
  • expression: The range to iterate over (e.g., an array, a std::vector, std::string, or an initializer list).
#include <iostream>
#include <vector>
#include <string>

int main() {
    // Vector
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    for (int num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    
    // C-style array
    int arr[] = {10, 20, 30, 40};
    for (int value : arr) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
    
    // String (iterates over characters)
    std::string text = "Hello";
    for (char c : text) {
        std::cout << c << " ";
    }
    std::cout << std::endl;
    
    // Initializer list
    for (double d : {1.1, 2.2, 3.3}) {
        std::cout << d << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

Comparison: Old vs. New Syntax

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

// Old way with index
for (size_t i = 0; i < numbers.size(); i++) {
    std::cout << numbers[i] << " ";
}

// Old way with iterators
for (std::vector<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) {
    std::cout << *it << " ";
}

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

Using auto Keyword

With the introduction of auto keyword in C++11, using auto in Range based for loops we can greatly reduce complexity as the porgrammer does not have to explicitly know the type of the entry in the containers or ranges. Simply use auto instead of the type. Compiler will deduce the type automatically from auto.

std::vector<std::string> words = {"hello", "world", "C++11"};

// Read-only, makes copies
for (auto word : words) {
    std::cout << word << " ";
}

Using References

So auto& can be used as well to get reference for entries that programmer can modify.

std::vector<std::string> words = {"hello", "world"};

// Read-only, no copies (efficient for large objects)
for (const auto& word : words) {
    std::cout << word << " ";
}

// Modify elements
for (auto& word : words) {
    word += "!";  // modifies the actual elements
}

How It Works Under the Hood

The Mechanism

The range-based for loop is essentially syntactic sugar that the compiler translates into a standard for loop that relies explicitly on iterators. This is why the underlying data structure needs begin() and end() functions.

Compiler Transformation

The C++ code you write:

for (const auto& element : container) {
    // user code
}

Is internally transformed by the compiler into something conceptually similar to:

{
    auto&& __range = container;
    auto __begin = begin(__range); // Calls the begin() function
    auto __end = end(__range);     // Calls the end() function

    for (; __begin != __end; ++__begin) {
        const auto& element = *__begin; // Uses operator* on the iterator
        // ... user loop body ...
    }
}

Why Iterators Are Necessary

The loop requires the begin() and end() functions to define the boundaries and the traversal logic:

  • begin(): Establishes the starting point of the iteration.
  • end(): Defines the termination condition (the loop stops when the current iterator equals the end iterator).
  • Iterators: The objects returned by these functions handle the mechanics of accessing (operator*) and moving to the next element (operator++).

Without begin() and end(), the compiler has no standardized way to obtain the starting and ending iterators required for this translation process to work.

Working with Different Container Types

Standard Containers

#include <vector>
#include <list>
#include <map>

// Vector
std::vector<int> vec = {1, 2, 3};
for (auto v : vec) {
    std::cout << v << " ";
}

// List
std::list<double> lst = {1.1, 2.2, 3.3};
for (const auto& l : lst) {
    std::cout << l << " ";
}

// Map
std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}};
for (const auto& pair : ages) {
    std::cout << pair.first << ": " << pair.second << std::endl;
}

Static Arrays

The range-based for loop works seamlessly with static (fixed-size) arrays because the compiler knows the exact size at compile time.

int static_array[] = {10, 20, 30, 40, 50};

for (int x : static_array) {
    std::cout << x << " ";
}

How It Works:

When you declare a static array, the compiler internally tracks both the memory location and the number of elements. The compiler calculates:

  1. begin(): The array name (decays to a pointer to the first element)
  2. end(): Uses pointer arithmetic with the known size (array + size)

The compiler treats it like:

auto* __begin = static_array;
auto* __end = static_array + 5; // '5' is known at compile time

Dynamic Arrays (Allocated with new)

The Problem

You cannot use a range-based for loop directly on a dynamically allocated array using new, because the compiler only sees a raw pointer (int*) and doesn’t know the size.

int* dynamicArray = new int[5];
// for (int x : dynamicArray) {} // Error: 'begin' was not found

Raw pointers don’t have begin() or end() member functions, and the compiler cannot determine the array size at compile time.

The Solution: Using Standard Library Helpers

You must explicitly provide the range boundaries using standard library functions.

Using std::ranges::subrange (C++20)

#include <iostream>
#include <ranges>

int main() {
    size_t size = 5;
    int* dynamicArray = new int[size];

    // Initialize the array
    for (size_t i = 0; i < size; ++i) {
        dynamicArray[i] = i * 10;
    }

    // Explicitly define the range using pointer arithmetic
    for (int x : std::ranges::subrange(dynamicArray, dynamicArray + size)) {
        std::cout << x << " "; // Output: 0 10 20 30 40
    }
    std::cout << std::endl;

    delete[] dynamicArray;
    return 0;
}
#include <iostream>
#include <span>

int main() {
    size_t size = 5;
    int* dynamicArray = new int[size];

    for (size_t i = 0; i < size; ++i) {
        dynamicArray[i] = i * 10;
    }

    // Wrap the pointer and size in a span
    std::span<int> span_of_array(dynamicArray, size);
    for (int x : span_of_array) {
        std::cout << x << " "; // Output: 0 10 20 30 40
    }
    std::cout << std::endl;

    delete[] dynamicArray;
    return 0;
}

By using std::ranges::subrange or std::span, you wrap your raw pointer and size into a type that satisfies the range concept (it has begin() and end() member functions), allowing the range-based for loop to work correctly.

Custom Classes and the Range Concept

To use a custom class with a range-based for loop, the class must satisfy the range concept.

Requirements

Your class must provide:

  1. begin() and end() functions, either as:

    • Member functions, or
    • Non-member functions in the same namespace (found via argument-dependent lookup)
  2. An iterator type that supports:

    • operator* (dereference)
    • operator!= (inequality comparison)
    • Pre-increment operator++

Example: Custom Container

#include <iostream>

class SimpleContainer {
private:
    int data[5] = {1, 2, 3, 4, 5};

public:
    // Iterator class
    class Iterator {
    private:
        int* ptr;
    
    public:
        Iterator(int* p) : ptr(p) {}
        
        // Dereference operator
        int& operator*() { return *ptr; }
        
        // Pre-increment operator
        Iterator& operator++() {
            ++ptr;
            return *this;
        }
        
        // Inequality comparison
        bool operator!=(const Iterator& other) const {
            return ptr != other.ptr;
        }
    };

    // begin() function
    Iterator begin() { return Iterator(data); }
    
    // end() function
    Iterator end() { return Iterator(data + 5); }
};

int main() {
    SimpleContainer container;
    
    for (int value : container) {
        std::cout << value << " "; // Output: 1 2 3 4 5
    }
    std::cout << std::endl;
    
    return 0;
}

Best Practices

Use const auto& for Read-Only Access

When you don’t need to modify elements and want to avoid copying (especially for large objects):

std::vector<std::string> large_strings = {"very", "long", "strings"};

for (const auto& str : large_strings) {
    std::cout << str << " ";
}

Use auto& for Modifications

When you need to modify the elements in place:

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

for (auto& num : numbers) {
    num *= 2;  // doubles each element
}

Use Plain auto for Copies

When you explicitly want to work with copies:

std::vector<int> numbers = {1, 2, 3};

for (auto num : numbers) {
    num *= 2;  // modifies the copy, not the original
}

Summary

Range-based for loops provide:

  • Cleaner syntax: No need for explicit iterators or index management
  • Less error-prone: Eliminates off-by-one errors and iterator mistakes
  • More readable: Intent is immediately clear
  • Flexible: Works with standard containers, arrays, and custom types

The key requirement is that the range must provide begin() and end() functions that return iterators supporting the basic iterator operations.