Coming Soon
This lesson is currently being developed
Range-based for loops (for-each)
Use modern C++ range-based for loops.
What to Expect
Comprehensive explanations with practical examples
Interactive coding exercises to practice concepts
Knowledge quiz to test your understanding
Step-by-step guidance for beginners
Development Status
Content is being carefully crafted to provide the best learning experience
Preview
Early Preview Content
This content is still being developed and may change before publication.
16.8 — Range-based for loops (for-each)
In this lesson, you'll master range-based for loops, also known as "for-each" loops. This modern C++ feature (introduced in C++11) provides a cleaner, safer, and more expressive way to iterate through containers like std::vector.
What are range-based for loops?
A range-based for loop is a simplified syntax for iterating through all elements in a container. Instead of managing indices or iterators manually, you can directly access each element in the container.
Think of it like this: instead of saying "go through positions 0, 1, 2... and get the element at each position," you say "give me each element in the container one by one."
Basic syntax
The basic syntax for a range-based for loop is:
for (declaration : container)
{
// Process each element
}
Simple example
#include <vector>
#include <iostream>
int main()
{
std::vector<int> numbers = {10, 20, 30, 40, 50};
// Traditional index-based loop
std::cout << "Traditional loop: ";
for (std::size_t i = 0; i < numbers.size(); ++i)
{
std::cout << numbers[i] << " ";
}
std::cout << std::endl;
// Range-based for loop (much cleaner!)
std::cout << "Range-based loop: ";
for (int number : numbers)
{
std::cout << number << " ";
}
std::cout << std::endl;
return 0;
}
Output:
Traditional loop: 10 20 30 40 50
Range-based loop: 10 20 30 40 50
Understanding the declaration part
The declaration in a range-based for loop can take several forms:
1. By value (creates a copy)
#include <vector>
#include <iostream>
#include <string>
int main()
{
std::vector<std::string> words = {"hello", "world", "cpp"};
// Each 'word' is a copy of the element
for (std::string word : words)
{
word = "modified"; // This only changes the copy
std::cout << word << " ";
}
std::cout << std::endl;
// Original vector is unchanged
std::cout << "Original: ";
for (const std::string& word : words)
{
std::cout << word << " ";
}
std::cout << std::endl;
return 0;
}
Output:
modified modified modified
Original: hello world cpp
2. By const reference (read-only, no copy)
#include <vector>
#include <iostream>
#include <string>
int main()
{
std::vector<std::string> words = {"hello", "world", "cpp"};
// Each 'word' is a const reference to the element (no copying)
for (const std::string& word : words)
{
std::cout << word.length() << " characters: " << word << std::endl;
// word = "modified"; // Error! Cannot modify const reference
}
return 0;
}
Output:
5 characters: hello
5 characters: world
3 characters: cpp
3. By non-const reference (can modify original)
#include <vector>
#include <iostream>
int main()
{
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::cout << "Original: ";
for (int num : numbers) std::cout << num << " ";
std::cout << std::endl;
// Each 'num' is a reference to the actual element
for (int& num : numbers) // Note the reference &
{
num *= 2; // This modifies the original element
}
std::cout << "After doubling: ";
for (int num : numbers) std::cout << num << " ";
std::cout << std::endl;
return 0;
}
Output:
Original: 1 2 3 4 5
After doubling: 2 4 6 8 10
4. Using auto (let the compiler deduce the type)
#include <vector>
#include <iostream>
#include <string>
int main()
{
std::vector<std::string> words = {"auto", "makes", "life", "easier"};
// auto deduces the type automatically
for (auto word : words) // auto becomes std::string (by value)
{
std::cout << word << " ";
}
std::cout << std::endl;
for (const auto& word : words) // auto becomes std::string (const reference)
{
std::cout << word.length() << " ";
}
std::cout << std::endl;
for (auto& word : words) // auto becomes std::string (non-const reference)
{
word = "modified";
}
for (const auto& word : words)
{
std::cout << word << " ";
}
std::cout << std::endl;
return 0;
}
Output:
auto makes life easier
4 5 4 6
modified modified modified modified
When to use each form
Use by value when:
- Elements are small (like int, char, bool)
- You need to modify the copy without affecting the original
- You're working with elements that are cheap to copy
std::vector<int> numbers = {1, 2, 3, 4, 5};
// Good: int is cheap to copy
for (int num : numbers)
{
std::cout << num * 2 << " "; // Use modified copy
}
Use const reference when:
- Elements are expensive to copy (like strings, custom objects)
- You only need to read the elements
- You want the best performance for read-only access
std::vector<std::string> words = {"hello", "world"};
// Good: avoid copying strings, read-only access
for (const auto& word : words)
{
std::cout << word.length() << std::endl;
}
Use non-const reference when:
- You need to modify the original elements
- Elements are expensive to copy
- You want to change the container's contents
std::vector<std::string> words = {"hello", "world"};
// Good: modify original elements without copying
for (auto& word : words)
{
word = "modified_" + word;
}
Comparing range-based loops with alternatives
Let's see how range-based for loops compare to other iteration methods:
#include <vector>
#include <iostream>
#include <chrono>
int main()
{
std::vector<int> largeVector(1000000, 42);
auto start = std::chrono::high_resolution_clock::now();
// Method 1: Index-based loop
start = std::chrono::high_resolution_clock::now();
long sum1 = 0;
for (std::size_t i = 0; i < largeVector.size(); ++i)
{
sum1 += largeVector[i];
}
auto indexTime = std::chrono::duration_cast<std::chrono::microseconds>
(std::chrono::high_resolution_clock::now() - start);
// Method 2: Iterator-based loop
start = std::chrono::high_resolution_clock::now();
long sum2 = 0;
for (auto it = largeVector.begin(); it != largeVector.end(); ++it)
{
sum2 += *it;
}
auto iteratorTime = std::chrono::duration_cast<std::chrono::microseconds>
(std::chrono::high_resolution_clock::now() - start);
// Method 3: Range-based for loop
start = std::chrono::high_resolution_clock::now();
long sum3 = 0;
for (int value : largeVector)
{
sum3 += value;
}
auto rangeTime = std::chrono::duration_cast<std::chrono::microseconds>
(std::chrono::high_resolution_clock::now() - start);
std::cout << "Index-based: " << indexTime.count() << " microseconds" << std::endl;
std::cout << "Iterator-based: " << iteratorTime.count() << " microseconds" << std::endl;
std::cout << "Range-based: " << rangeTime.count() << " microseconds" << std::endl;
std::cout << "All sums equal: " << (sum1 == sum2 && sum2 == sum3) << std::endl;
return 0;
}
Output (example):
Index-based: 1234 microseconds
Iterator-based: 1198 microseconds
Range-based: 1203 microseconds
All sums equal: 1
Range-based for loops typically have performance similar to iterators and are often optimized by the compiler.
Working with different container types
Range-based for loops work with many different types of containers:
#include <vector>
#include <array>
#include <string>
#include <iostream>
int main()
{
// std::vector
std::vector<int> vec = {1, 2, 3, 4, 5};
std::cout << "Vector: ";
for (int val : vec) std::cout << val << " ";
std::cout << std::endl;
// std::array
std::array<double, 4> arr = {1.1, 2.2, 3.3, 4.4};
std::cout << "Array: ";
for (double val : arr) std::cout << val << " ";
std::cout << std::endl;
// std::string
std::string text = "Hello";
std::cout << "String: ";
for (char c : text) std::cout << c << " ";
std::cout << std::endl;
// C-style array
int cArray[] = {10, 20, 30, 40};
std::cout << "C-array: ";
for (int val : cArray) std::cout << val << " ";
std::cout << std::endl;
// Initializer list
std::cout << "Initializer list: ";
for (int val : {100, 200, 300}) std::cout << val << " ";
std::cout << std::endl;
return 0;
}
Output:
Vector: 1 2 3 4 5
Array: 1.1 2.2 3.3 4.4
String: H e l l o
C-array: 10 20 30 40
Initializer list: 100 200 300
Nested range-based loops
You can use range-based for loops with multi-dimensional containers:
#include <vector>
#include <iostream>
int main()
{
// 2D vector (vector of vectors)
std::vector<std::vector<int>> matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
std::cout << "Matrix using nested range-based loops:" << std::endl;
for (const auto& row : matrix) // Each row is a vector<int>
{
for (int value : row) // Each value is an int
{
std::cout << value << "\t";
}
std::cout << std::endl;
}
// Calculate sum of all elements
int sum = 0;
for (const auto& row : matrix)
{
for (int value : row)
{
sum += value;
}
}
std::cout << "Sum of all elements: " << sum << std::endl;
return 0;
}
Output:
Matrix using nested range-based loops:
1 2 3
4 5 6
7 8 9
Sum of all elements: 45
Practical examples
Example 1: Processing student grades
#include <vector>
#include <iostream>
#include <string>
struct Student
{
std::string name;
std::vector<double> grades;
double getAverage() const
{
if (grades.empty()) return 0.0;
double sum = 0.0;
for (double grade : grades) // Range-based for loop
{
sum += grade;
}
return sum / grades.size();
}
};
int main()
{
std::vector<Student> students = {
{"Alice", {95.0, 87.5, 92.0, 88.5}},
{"Bob", {78.5, 82.0, 79.5, 85.0}},
{"Charlie", {91.0, 94.5, 89.0, 93.5}}
};
std::cout << "Student Grade Report:" << std::endl;
std::cout << "===================" << std::endl;
for (const auto& student : students) // Iterate through students
{
std::cout << student.name << ": ";
// Print individual grades
for (double grade : student.grades) // Iterate through grades
{
std::cout << grade << " ";
}
std::cout << "(Average: " << student.getAverage() << ")" << std::endl;
}
return 0;
}
Output:
Student Grade Report:
===================
Alice: 95 87.5 92 88.5 (Average: 90.75)
Bob: 78.5 82 79.5 85 (Average: 81.25)
Charlie: 91 94.5 89 93.5 (Average: 92)
Example 2: Text processing
#include <vector>
#include <iostream>
#include <string>
int main()
{
std::vector<std::string> sentences = {
"Hello world",
"This is C++ programming",
"Range-based loops are great",
"They make code cleaner"
};
std::cout << "Text Analysis:" << std::endl;
std::cout << "==============" << std::endl;
int totalWords = 0;
int totalCharacters = 0;
for (const auto& sentence : sentences) // Process each sentence
{
// Count words (simplified: count spaces + 1)
int wordCount = 1;
for (char c : sentence) // Process each character
{
if (c == ' ')
{
++wordCount;
}
}
totalWords += wordCount;
totalCharacters += sentence.length();
std::cout << "\"" << sentence << "\"" << std::endl;
std::cout << " Words: " << wordCount << ", Characters: " << sentence.length() << std::endl;
}
std::cout << "\nSummary:" << std::endl;
std::cout << "Total sentences: " << sentences.size() << std::endl;
std::cout << "Total words: " << totalWords << std::endl;
std::cout << "Total characters: " << totalCharacters << std::endl;
std::cout << "Average words per sentence: " << (double)totalWords / sentences.size() << std::endl;
return 0;
}
Output:
Text Analysis:
==============
"Hello world"
Words: 2, Characters: 11
"This is C++ programming"
Words: 4, Characters: 23
"Range-based loops are great"
Words: 4, Characters: 27
"They make code cleaner"
Words: 4, Characters: 22
Summary:
Total sentences: 4
Total words: 14
Total characters: 83
Average words per sentence: 3.5
Limitations of range-based for loops
While range-based for loops are great, they have some limitations:
1. No access to index
#include <vector>
#include <iostream>
int main()
{
std::vector<std::string> items = {"apple", "banana", "cherry"};
// ❌ Can't get index directly with range-based loop
std::cout << "Items (manual index):" << std::endl;
int index = 0;
for (const auto& item : items)
{
std::cout << index << ": " << item << std::endl;
++index;
}
// ✅ Traditional loop when you need the index
std::cout << "\nItems (traditional loop):" << std::endl;
for (std::size_t i = 0; i < items.size(); ++i)
{
std::cout << i << ": " << items[i] << std::endl;
}
return 0;
}
Output:
Items (manual index):
0: apple
1: banana
2: cherry
Items (traditional loop):
0: apple
1: banana
2: cherry
2. Can't skip elements easily
#include <vector>
#include <iostream>
int main()
{
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// ❌ Can't easily skip elements with range-based loop
std::cout << "All numbers: ";
for (int num : numbers)
{
std::cout << num << " ";
}
std::cout << std::endl;
// ✅ Traditional loop for skipping elements (every other element)
std::cout << "Every other number: ";
for (std::size_t i = 0; i < numbers.size(); i += 2)
{
std::cout << numbers[i] << " ";
}
std::cout << std::endl;
return 0;
}
Output:
All numbers: 1 2 3 4 5 6 7 8 9 10
Every other number: 1 3 5 7 9
3. Can't iterate backwards easily
#include <vector>
#include <iostream>
int main()
{
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::cout << "Forward: ";
for (int num : numbers)
{
std::cout << num << " ";
}
std::cout << std::endl;
// ❌ No direct way to iterate backwards with range-based loop
// ✅ Use reverse iterators instead
std::cout << "Backward: ";
for (auto it = numbers.rbegin(); it != numbers.rend(); ++it)
{
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
Output:
Forward: 1 2 3 4 5
Backward: 5 4 3 2 1
Best practices
1. Choose the right declaration type
// ✅ Good: Use const reference for read-only access to expensive-to-copy objects
for (const std::string& word : words) { /* read only */ }
// ✅ Good: Use non-const reference to modify original elements
for (std::string& word : words) { word = processWord(word); }
// ✅ Good: Use by value for small, cheap-to-copy types
for (int number : numbers) { /* process copy */ }
// ❌ Avoid: By value for expensive-to-copy objects when you don't need a copy
for (std::string word : words) { /* unnecessary copying */ }
2. Use auto when appropriate
// ✅ Good: Let the compiler deduce the type
for (const auto& item : container) { /* compiler figures out the type */ }
// ✅ Also good: Explicit type when clarity is important
for (const std::string& word : words) { /* clear what type we're working with */ }
3. Know when NOT to use range-based loops
// Use traditional loops when:
// - You need the index
// - You need to skip elements
// - You need to iterate backwards
// - You need to modify the container size during iteration
// - You need complex iteration patterns
Summary
Range-based for loops (for-each loops) are a powerful C++11 feature that makes code cleaner and safer:
Advantages:
- Cleaner, more readable syntax
- No risk of index errors or iterator invalidation
- Works with any container type
- Often as fast as traditional loops
Key concepts:
- Use
const auto&
for read-only access to avoid copying - Use
auto&
when you need to modify elements - Use
auto
(by value) for small types or when you need a copy
When to use range-based loops:
- Simple iteration over all elements
- When you don't need indices or complex navigation
- When readability is important
- For processing elements sequentially
When to use traditional loops:
- When you need element indices
- When you need to skip elements or iterate backwards
- When you need to modify container size during iteration
- For complex iteration patterns
In the next lesson, you'll learn about using enumerators with array indexing for better code organization.
Quiz
- What are the three main ways to declare the variable in a range-based for loop?
- When should you use
const auto&
vsauto&
vsauto
in a range-based for loop? - What are three limitations of range-based for loops compared to traditional index-based loops?
- How do you modify the original elements of a vector using a range-based for loop?
- Can you use range-based for loops with C-style arrays? Why or why not?
Practice exercises
Try these exercises to practice using range-based for loops:
-
Grade processor: Create a program that takes a vector of student test scores and uses range-based for loops to calculate the average, find the highest and lowest scores, and count how many scores are above the average.
-
Text analyzer: Write a program that analyzes a vector of strings using range-based for loops to count total words, find the longest word, and convert all text to uppercase.
-
Matrix operations: Create a 2D vector and use nested range-based for loops to perform operations like finding the sum of all elements, the maximum element in each row, and transposing the matrix.
-
Container comparison: Write a template function that uses range-based for loops to compare two containers of any type for equality, handling different container types appropriately.
Explore More Courses
Discover other available courses while this lesson is being prepared.
Browse CoursesLesson Discussion
Share your thoughts and questions