Coming Soon

This lesson is currently being developed

Range-based for loops (for-each)

Use modern C++ range-based for loops.

Dynamic arrays: std::vector
Chapter
Beginner
Difficulty
45min
Estimated Time

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

In Progress

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

  1. What are the three main ways to declare the variable in a range-based for loop?
  2. When should you use const auto& vs auto& vs auto in a range-based for loop?
  3. What are three limitations of range-based for loops compared to traditional index-based loops?
  4. How do you modify the original elements of a vector using a range-based for loop?
  5. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Continue Learning

Explore other available lessons while this one is being prepared.

View Course

Explore More Courses

Discover other available courses while this lesson is being prepared.

Browse Courses

Lesson Discussion

Share your thoughts and questions

💬

No comments yet. Be the first to share your thoughts!

Sign in to join the discussion