Range-based for loops (for-each)

Traditional for loops work well for array traversal, but they're verbose, require index management, and are prone to off-by-one errors. C++ offers a cleaner alternative: the range-based for loop.

Traditional loop limitations

Here's a standard loop iterating through a vector:

#include <iostream>
#include <vector>

int main()
{
    std::vector scores{ 85, 92, 78, 90, 88 };

    for (std::size_t idx{ 0 }; idx < scores.size(); ++idx)
        std::cout << scores[idx] << ' ';

    std::cout << '\n';

    return 0;
}

This requires declaring the index variable, checking bounds, incrementing correctly, and indexing the vector—all opportunities for bugs.

Introducing range-based for loops

Range-based for loops (also called for-each loops) simplify traversal:

for (element_declaration : container)
    statement;

The loop iterates through each element in container, assigning the current element to the variable declared in element_declaration, then executes statement.

Here's the previous example rewritten:

#include <iostream>
#include <vector>

int main()
{
    std::vector scores{ 85, 92, 78, 90, 88 };

    for (int score : scores)
        std::cout << score << ' ';

    std::cout << '\n';

    return 0;
}

Much cleaner! No indices, no size checks, no manual incrementing. The loop automatically iterates through all elements.

Core Understanding: The declared element (score) receives the value of each array element, not an index. For each iteration, the current element is copied into score.

Best practice: Favor range-based for loops over traditional for loops when traversing containers.

Range-based loops with empty containers

If the container is empty, the loop body simply doesn't execute:

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> empty{};

    for (int val : empty)
        std::cout << "This never prints\n";

    std::cout << "Loop completed\n";

    return 0;
}

Output: Loop completed

Using auto for type deduction

Since the element declaration should match the element type, auto is perfect here:

#include <iostream>
#include <vector>

int main()
{
    std::vector scores{ 85, 92, 78, 90, 88 };

    for (auto score : scores)  // Compiler deduces int
        std::cout << score << ' ';

    std::cout << '\n';

    return 0;
}

Benefits:

  • No type redundancy
  • Automatically adapts if element type changes
  • Prevents accidental type mismatches and conversions

Best practice: Use auto with range-based for loops to have the compiler deduce the element type.

Avoiding copies with references

Consider iterating over a vector of strings:

#include <iostream>
#include <string>
#include <vector>

int main()
{
    std::vector<std::string> names{ "Alice", "Bob", "Charlie", "Diana" };

    for (auto name : names)  // Copies each string!
        std::cout << name << ' ';

    std::cout << '\n';

    return 0;
}

Each iteration copies an entire string—expensive! Use a reference instead:

#include <iostream>
#include <string>
#include <vector>

int main()
{
    std::vector<std::string> names{ "Alice", "Bob", "Charlie", "Diana" };

    for (const auto& name : names)  // No copying
        std::cout << name << ' ';

    std::cout << '\n';

    return 0;
}

The reference binds directly to each element, avoiding copies. Use const when you don't need to modify elements.

If you need to modify elements, omit const:

#include <iostream>
#include <vector>

int main()
{
    std::vector damage{ 10, 20, 30, 40 };

    for (auto& dmg : damage)
        dmg *= 2;  // Double each value

    for (auto dmg : damage)
        std::cout << dmg << ' ';  // Prints: 20 40 60 80

    return 0;
}

Choosing between auto, auto&, and const auto&

General guidelines:

  • Use auto when working with cheap-to-copy types (like int) and copies are acceptable
  • Use auto& when you need to modify elements
  • Use const auto& for expensive-to-copy types or when you want to guarantee no modifications

However, many developers prefer always using const auto& for range-based loops because it's more future-proof. If the element type later changes from cheap-to-copy to expensive-to-copy, const auto& continues working efficiently.

Best practice: For range-based for loops, prefer:

  • auto when modifying copies
  • auto& when modifying original elements
  • const auto& when just viewing elements (most common)

Range-based loops with other container types

Range-based for loops work with many container types, including std::array, C-style arrays, strings, and various other standard containers:

#include <array>
#include <iostream>

int main()
{
    std::array levels{ 5, 12, 8, 15, 20 };

    for (const auto& level : levels)
        std::cout << level << ' ';

    std::cout << '\n';

    return 0;
}

Limitations of range-based for loops

Range-based for loops don't directly provide element indices. If you need indices, use a traditional loop or manually maintain a counter:

#include <iostream>
#include <vector>

int main()
{
    std::vector inventory{ "sword", "shield", "potion" };

    int idx{ 0 };
    for (const auto& item : inventory)
    {
        std::cout << idx << ": " << item << '\n';
        ++idx;
    }

    return 0;
}

Though if you need indices, consider whether a traditional loop might be clearer.

Iterating in reverse (C++20)

Use std::views::reverse to iterate backwards:

#include <iostream>
#include <ranges>
#include <vector>

int main()
{
    std::vector ranks{ 1, 2, 3, 4, 5 };

    for (const auto& rank : std::views::reverse(ranks))
        std::cout << rank << ' ';  // Prints: 5 4 3 2 1

    std::cout << '\n';

    return 0;
}