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 iterationcollection
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:
- Member functions
begin()
andend()
, or - Be usable with free functions
begin()
andend()
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:
-
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.)
-
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
-
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
-
Cannot track indices: Range-based for doesn't provide the index of the current element. If you need indices, consider:
cppfor (size_t i = 0; i < vec.size(); ++i) {
// Use vec[i] and have index i
} -
Cannot modify the container structure: Adding or removing elements during iteration may lead to undefined behavior.
-
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:
- Implement
begin()
andend()
member functions or free functions, or - 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
-
Write a program that uses a range-based for loop to find the largest element in a vector of integers.
-
Create a function that takes a vector of strings and returns a new vector containing only strings that start with a specific letter.
-
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.
-
Write a program that uses a range-based for loop with a map to count the frequency of words in a string.
-
Refactor the following code to use a range-based for loop:
cppstd::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! :)