Ranges and Views

This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com.

This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

C++20’s <ranges> builds on the iterator concepts from Iterators and Algorithms: a range is anything with a begin()/end() pair (a concept, not a concrete type), letting algorithms and views take a whole container instead of an iterator pair.

std::ranges Constrained Algorithms

Every <algorithm> function has a std::ranges:: counterpart taking a range directly, constrained by concepts (clearer errors on misuse) and supporting projections:

#include <algorithm>
#include <vector>
#include <string>

struct Person { std::string name; int age; };

int main() {
    std::vector<int> v = {5, 3, 1, 4, 2};
    std::ranges::sort(v);                          // no .begin()/.end() needed

    std::vector<Person> people = {{"Ada", 36}, {"Bob", 25}};
    std::ranges::sort(people, {}, &Person::age);     // projection: sort BY age, comparator defaulted to std::less
    auto it = std::ranges::find(people, 25, &Person::age);   // find the person whose age == 25
    (void)it;
}

The projection parameter (&Person::age above) is what elevates std::ranges:: algorithms above their <algorithm> predecessors for everyday use — no more writing a one-off lambda just to compare by a field.

Views and Range Adaptors

A view is a lightweight, non-owning, typically lazy range — adaptors compose with | (pipe), evaluating element-by-element only as the result is actually iterated, not eagerly up front:

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

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    auto result = v
        | std::views::filter([](int x) { return x % 2 == 0; })   // keep evens
        | std::views::transform([](int x) { return x * x; })      // square them
        | std::views::take(3);                                     // first 3 results

    for (int x : result) std::cout << x << ' ';   // 4 16 36 -- computed lazily, one element at a time

    for (int x : v | std::views::drop(5)) std::cout << x << ' ';   // 6 7 8 9 10
}

C++23 added several more adaptors that previously needed hand-rolled loops or third-party ranges libraries:

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

int main() {
    std::vector<int> a = {1, 2, 3};
    std::vector<char> b = {'x', 'y', 'z'};

    for (auto [num, ch] : std::views::zip(a, b)) {          // C++23: pair up two ranges element-wise
        std::cout << num << ch << ' ';                        // 1x 2y 3z
    }

    for (auto [i, ch] : std::views::enumerate(b)) {           // C++23: (index, element) pairs
        std::cout << i << ':' << ch << ' ';                     // 0:x 1:y 2:z
    }

    for (auto group : a | std::views::chunk(2)) {              // C++23: fixed-size sub-ranges
        for (int x : group) std::cout << x << ' ';               // {1,2} then {3}
        std::cout << "| ";
    }
}

ranges::to

C++23’s std::ranges::to materializes a lazy view into a concrete container, replacing the std::copy-into-a-preallocated-container idiom views otherwise require:

#include <ranges>
#include <vector>

std::vector<int> v = {1, 2, 3, 4, 5};
auto evens = v | std::views::filter([](int x) { return x % 2 == 0; }) | std::ranges::to<std::vector>();
// evens is a real, owning std::vector<int>{2, 4}, not a lazy view
std::ranges::to is standard-conformant C++23 (verified against cppreference and the working draft) but is not yet available in this environment’s libstdc++ 13 — it needs GCC 14+, a recent libc++, or MSVC (see Getting Started); every other range adaptor on this page, including zip/enumerate/chunk above, does compile locally.

Writing a Custom View

A minimal custom view needs begin()/end() returning iterators/sentinels satisfying the relevant concepts; inheriting from std::ranges::view_interface fills in the rest (empty(), front(), back(), operator[], …​) from just begin()/end():

#include <ranges>
#include <iterator>

class RepeatView : public std::ranges::view_interface<RepeatView> {
public:
    RepeatView(int value, int count) : value_(value), count_(count) {}

    struct Iterator {
        using iterator_category = std::input_iterator_tag;
        using value_type = int;
        using difference_type = std::ptrdiff_t;

        int value; int remaining;
        int operator*() const { return value; }
        Iterator& operator++() { --remaining; return *this; }
        Iterator operator++(int) { auto tmp = *this; ++*this; return tmp; }
        bool operator==(std::default_sentinel_t) const { return remaining == 0; }
    };

    Iterator begin() const { return Iterator{value_, count_}; }
    std::default_sentinel_t end() const { return {}; }

private:
    int value_, count_;
};
// for (int x : RepeatView(7, 3)) { ... }   // yields 7, 7, 7

Lazy View Pipeline

flowchart LR A["vector<int> v"] --> B["views::filter(even)"] B --> C["views::transform(square)"] C --> D["views::take(3)"] D --> E["range-for pulls one
element at a time"] E -.->|"pulls next"| B note1["Nothing is computed until E
actually asks for the next element --
each adaptor is a thin, lazy wrapper."]