Range & View

In the C++20 standard library, the Ranges library introduces a novel abstraction for manipulating sequences of elements. Two fundamental concepts within this library are ranges and views.

Range

A range is an abstraction that represents a sequence of elements that can be traversed. Conceptually, it encapsulates the notions of “beginning” and “end” into a unified entity. Traditionally, C++ iteration involves pairs of iterators, such as [begin, end). The Ranges library simplifies this process by introducing the std::ranges::range concept, which merges these iterators into a more cohesive structure.

  • A range can be thought of as “something you can iterate over.”
  • For instance, std::vector<int> is a range. A built-in array int arr[10] can be adapted to a range. Many STL containers can seamlessly act as ranges.

View

A view is a specialized type of range. It is a lightweight, composable, and non-owning adaptor that transforms or filters another range. Views do not store the entire set of elements; instead, they typically refer to another underlying range and apply a transformation “on-the-fly” during iteration. This property of views makes them often lazy: they only compute values as you iterate through them.

auto result = some_range 
            | std::views::filter([](int x) { return x % 2 == 0; })
            | std::views::transform([](int x) { return x * x; });

Putting It All Together

The introduction of ranges and views enables a more functional, pipeline-oriented programming style using standard C++. Instead of manually writing loops and constructing temporary vectors, these abstractions can be directly connected. For instance:

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

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5, 6};

    auto even_squares = vec
                       | std::views::filter([](int x) { return x % 2 == 0; })
                       | std::views::transform([](int x) { return x * x; });

    for (int x : even_squares) {
        std::cout << x << " ";
    }
    
}

This snippet demonstrates the ease of chaining transformations and filters together using views. The vec represents a range, and even_squares is a view pipeline that lazily filters and transforms vec’s elements as we traverse it.

Filtering and Transforming a std::vector

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

int main() {
    std::vector<int> numbers = {10, 1, 42, -5, 0, 7, 2};

    // Filter out negative numbers and zeros, then double the remaining ones.
    auto positive_doubled = numbers 
                          | std::views::filter([](int x) { return x > 0; })
                          | std::views::transform([](int x) { return x * 2; });
                          
    for (int val : positive_doubled) {
        std::cout << val << " ";
    }
    

    return 0;
}

Using std::array with take and reverse Views

#include <iostream>
#include <array>
#include <ranges>

int main() {
    std::array<int, 5> arr = {5, 10, 15, 20, 25};

    // Take the first 3 elements and then reverse them
    auto first_three_reversed = arr 
                              | std::views::take(3) 
                              | std::views::reverse;

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

    return 0;
}

Using transform on a std::string to Change Case

#include <iostream>
#include <string>
#include <ranges>
#include <cctype>

int main() {
    std::string text = "Hello, Ranges!";

    // Transform each character to uppercase
    auto upper_view = text 
                    | std::views::transform([](unsigned char c) { return static_cast<char>(std::toupper(c)); });

    for (char c : upper_view) {
        std::cout << c;
    }

    return 0;
}

Key Takeaways:

  • Views are lightweight, non-owning adapters that apply transformations or filters lazily as you iterate.
  • They can be chained together with the | (pipe) operator, making code more readable and expressive.
  • They work on any range, including STL containers (std::vector, std::array, std::deque, std::string) and special views like iota.
  • They encourage a functional programming style in C++, reducing the need for manual loops, temporary containers, and complex iterator manipulations.