The Challenge of Differing Iterator Types in C++

In C++, std::vector<T>::iterator and std::vector<T>::reverse_iterator are completely different, incompatible types. Because C++ is a statically-typed language, you cannot declare a single pair of iterator variables (like it_b and it_e) and assign either forward or reverse iterators to them at runtime using a simple if-else block.

However, there are several elegant, modern, and highly efficient ways to achieve this behavior without duplicating your loop logic. Let's explore the best solutions, ranging from classic C++ templates to modern C++20/C++23 ranges.

Solution 1: The Idiomatic STL Way (Generic Lambda / Helper Function)

Instead of trying to store the iterators in the outer scope, the standard and most performant C++ approach is to pass the iterators to a template function or a generic lambda. This allows the compiler to generate the correct loop code for both iterator types with zero runtime overhead.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> nums = {10, 20, 30, 40, 50};
    int n = -1; // Condition variable

    // Define a generic lambda to handle the loop logic
    auto process_loop = [](auto begin, auto end) {
        for (auto it = begin; it != end; ++it) {
            std::cout << *it << " ";
        }
        std::cout << "\n";
    };

    // Conditionally pass the appropriate iterators
    if (n >= 0) {
        process_loop(nums.begin(), nums.end());
    } else {
        process_loop(nums.rbegin(), nums.rend());
    }

    return 0;
}

Solution 2: Modern C++20 Ranges and Views

If you are using C++20, you can avoid dealing with raw iterators altogether. Instead of conditionally selecting iterators, you can conditionally apply a reverse view to your container. This keeps your code clean, readable, and highly declarative.

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

int main() {
    std::vector<int> nums = {10, 20, 30, 40, 50};
    int n = -1;

    auto execute_loop = [](auto&& range) {
        for (int val : range) {
            std::cout << val << " ";
        }
        std::cout << "\n";
    };

    if (n >= 0) {
        execute_loop(nums);
    } else {
        execute_loop(nums | std::views::reverse);
    }
}

Solution 3: Type Erasure with C++23 std::ranges::any_view

If you absolutely need a single variable to hold either the forward range or the reversed range, C++23 introduces std::ranges::any_view. This class template provides type erasure for views, allowing you to reassign different view types to the same variable.

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

int main() {
    std::vector<int> nums = {10, 20, 30, 40, 50};
    int n = -1;

    // Type-erased view that yields 'int&'
    std::ranges::any_view<int> view;

    if (n >= 0) {
        view = nums;
    } else {
        view = nums | std::views::reverse;
    }

    for (int val : view) {
        std::cout << val << " ";
    }
}

Summary: Which Approach Should You Choose?

  • Use a Generic Lambda (Solution 1) if you want a highly performant, zero-overhead solution that works seamlessly across C++14 and later.
  • Use C++20 Views (Solution 2) for modern, readable code that avoids raw iterator manipulation.
  • Use C++23 any_view (Solution 3) if you strictly require a single variable to represent the conditional range.