Skip to content
C++
Library
since C++20
Intermediate

Ranges Library

The C++20 ranges library provides composable, lazy sequences with concept-constrained algorithms and view adaptors via the pipe operator.

Ranges Librarysince C++20

The <ranges> library introduces a unified model of element sequences built on concepts, providing lazy view adaptors that compose via operator| and constrained algorithm overloads in the std::ranges namespace.

Overview

Pre-C++20 STL algorithms operate on iterator pairs, which is expressive but verbose and error-prone β€” passing mismatched begin/end pairs from different containers is undefined behaviour with no diagnostic. The ranges library addresses this by treating a range as the primary abstraction: any type that exposes ranges::begin() and ranges::end().

Three things come with the library:

  1. Concepts β€” a hierarchy of named constraints (range, sized_range, input_range, forward_range, bidirectional_range, random_access_range, contiguous_range) that replace informal iterator category conventions with enforced requirements.
  2. Constrained algorithms β€” every <algorithm> has a std::ranges:: counterpart that accepts a range directly and supports projections.
  3. Views β€” lightweight, lazily-evaluated adaptors in std::views (alias for std::ranges::views) that can be chained via operator|.

Views are the most distinctive feature. A view does not own elements; it represents a transformed window over some underlying range. Composition is declarative and evaluates only when iterated, so chaining ten adaptors doesn't produce ten temporary containers.

Syntax

cpp
#include <ranges>          // views, range concepts, range utilities
#include <algorithm>       // std::ranges:: algorithm overloads

namespace rv = std::views; // std::views is an alias for std::ranges::views (C++20)

Core concept definitions (simplified from the standard):

cpp
// C++20 β€” from <ranges>
template<class T>
concept range = requires(T& t) {
    std::ranges::begin(t);
    std::ranges::end(t);
};

template<class T>
concept sized_range = std::ranges::range<T> && requires(T& t) {
    std::ranges::size(t);
};

template<class T>
concept random_access_range =
    std::ranges::bidirectional_range<T> &&
    std::random_access_iterator<std::ranges::iterator_t<T>>;

Examples

Range algorithms with projections

std::ranges:: algorithms accept a range directly and an optional projection β€” a callable applied to each element before comparison. This eliminates the need for custom comparators that unwrap wrapper types:

cpp
#include <algorithm>
#include <ranges>
#include <string>
#include <vector>

struct Employee {
    std::string name;
    int salary;
};

int main() {
    std::vector<Employee> staff = {
        {"Alice", 95000}, {"Bob", 72000}, {"Carol", 110000}
    };

    // C++20: sort by salary via projection, no lambda comparator needed
    std::ranges::sort(staff, std::less{}, &Employee::salary);

    // C++20: find by name projection
    auto it = std::ranges::find(staff, "Bob", &Employee::name);
}

Composing views with the pipe operator

Views are lazily evaluated and compose left-to-right via operator|. No intermediate containers are created:

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

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

    // C++20: filter even numbers, square them, take the first 3
    auto pipeline = numbers
        | std::views::filter([](int n) { return n % 2 == 0; })
        | std::views::transform([](int n) { return n * n; })
        | std::views::take(3);

    for (int v : pipeline)
        std::cout << v << ' '; // 4 16 36
}

Generating sequences with iota

std::views::iota produces an infinite or bounded sequence without storing it:

cpp
#include <ranges>
#include <vector>

// C++20: generate [0, 100) stride 5, collect to vector
// C++23: std::views::stride and std::ranges::to<>
auto stepped = std::views::iota(0, 100)
             | std::views::stride(5)          // C++23
             | std::ranges::to<std::vector>(); // C++23
// stepped == {0, 5, 10, ..., 95}

Flattening nested ranges

cpp
#include <ranges>
#include <vector>
#include <string>
#include <iostream>

int main() {
    std::vector<std::string> words = {"hello", " ", "world"};

    // C++20: join_view flattens a range-of-ranges
    for (char c : words | std::views::join)
        std::cout << c; // hello world
}

C++23 sliding-window adaptors

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

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

    // C++23: adjacent<N> produces tuples of N consecutive elements
    for (auto [a, b] : vec | std::views::adjacent<2>)
        std::cout << '(' << a << ',' << b << ") "; // (1,2) (2,3) (3,4) (4,5)

    std::cout << '\n';

    // C++23: adjacent_transform applies a function across each window
    for (auto s : vec | std::views::adjacent_transform<2>(std::plus{}))
        std::cout << s << ' '; // 3 5 7 9
}

Constraining a template on a range concept

cpp
#include <ranges>

// C++20: accepts only random-access ranges β€” enforced at compile time
template<std::ranges::random_access_range R, typename Comp = std::less<>>
void quicksort(R&& r, Comp cmp = {}) {
    if (std::ranges::size(r) < 2) return;
    // ...
}

Best Practices

Prefer std::ranges:: algorithms over raw iterator algorithms. The constrained overloads catch type mismatches at compile time and accept projections without helper lambdas. The interface is strictly better where available.

Use namespace rv = std::views; in implementation files. The full std::ranges::views:: prefix is verbose. In headers, qualify fully to avoid polluting client namespaces.

Treat views as cheap to copy, not to hold long-term without the underlying range. A view is a non-owning reference. Storing a filter_view after the source vector is destroyed yields dangling iterators.

Use std::ranges::to<std::vector>() (C++23) to materialise a pipeline. Before C++23, you need to pipe into std::back_inserter explicitly. In C++23, | std::ranges::to<std::vector>() is both cleaner and potentially more efficient.

Prefer std::views::iota over hand-rolled index loops when you only need a numeric sequence. It composes naturally with the rest of the pipeline, whereas a raw loop requires breaking out of the declarative chain.

Common Pitfalls

Dangling views over temporaries. auto v = std::string{"hello"} | std::views::reverse; binds a view to a destroyed temporary. Clang and GCC warn in some cases, but not all. Always ensure the source range outlives its views.

filter_view invalidates size information. Unlike transform_view, a filtered range cannot report its size cheaply. Code that calls std::ranges::size() on a filter_view will fail to compile β€” filter_view is not a sized_range. Cache the size explicitly if you need it.

Mutable elements through transform_view. transform_view returns rvalues from its iterator dereference. You cannot write *it = 42 through a transform view. Use std::ranges::for_each or a for loop over the source range if you need mutation.

O(n) begin() on filter_view and drop_while_view. These views must scan forward to find the first matching element every time begin() is called on a non-cached instance. Iterating the same filtered view twice in a loop body incurs the scan cost twice. Assign the view to a local variable so begin() is computed once.

Not all range algorithms are constrained the same way. std::ranges::sort requires random_access_range; std::ranges::stable_sort additionally requires sized_range. Passing a filter_view to sort is a hard compile error, which is intentional β€” it prevents accidental O(nΒ²) iterator advancement β€” but the error message can be dense.

std::views::stride and std::ranges::to are C++23. Projects targeting C++20 must implement stride manually (e.g., filter on index modulo) and materialise pipelines via std::vector<T>(begin, end).

See Also

  • std::views β€” the full catalogue of view adaptors available in C++20 and C++23
  • Range concepts (range, sized_range, view, input_range, etc.) β€” the concept hierarchy that underpins the design
  • std::ranges:: algorithm overloads β€” constrained counterparts to every <algorithm> function