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 likeint,std::string, or useauto)variable: The name of the variable that will hold each elementdeclaration: The complete variable declaration (type variable), whose type must be compatible with the element type of the sequence. Theautokeyword is highly recommended here.expression: The range to iterate over (e.g., an array, astd::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 theenditerator).- 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:
begin(): The array name (decays to a pointer to the first element)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;
}
Using std::span (C++20 - Recommended)
#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:
-
begin()andend()functions, either as:- Member functions, or
- Non-member functions in the same namespace (found via argument-dependent lookup)
-
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.