Skip to content

Range Adaptors, Views, and Composition

Range Adaptors, Views, and Composition Pipelines

Section titled “Range Adaptors, Views, and Composition Pipelines”

Range adaptors are lazy, composable transformations applied to ranges via the pipe operator |. Each adaptor returns a view --- a lightweight object that refers to underlying elements without Owning them. This section covers the standard adaptors, lazy evaluation semantics, pipe-based Composition, and practical data processing pipelines.

Range adaptors are lazy, composable transformations applied to ranges via the pipe operator | [N4950 §26.5.2]. Each adaptor returns a view --- a lightweight object that refers to the Underlying elements without owning them. Views satisfy std::ranges::view [N4950 §26.5.2] and have O(1)O(1) construction and destruction.

The standard library provides these range adaptors [N4950 §26.5.2 Table 96]:

AdaptorDescription
views::filter(pred)Elements satisfying predicate
views::transform(f)Apply function to each element
views::take(n)First n elements
views::drop(n)Skip first n elements
views::reverseReverse order
views::zip(r1, r2, ...)Zip multiple ranges into tuples
views::split(delim)Split by delimiter
views::joinFlatten a range of ranges
views::enumeratePair each element with its index (C++23)
views::iota(start)Infinite sequence from start
views::keysExtract keys from associative containers
views::valuesExtract values from associative containers
views::take_while(pred)Take elements while predicate holds
views::drop_while(pred)Drop elements while predicate holds
views::elements<N>Extract Nth element from tuple-like values
views::transform(f) | views::filter(pred)Composition via pipe

Lazy Evaluation: Views Are Composable Without Materialization

Section titled “Lazy Evaluation: Views Are Composable Without Materialization”

Views are lazy: no computation occurs until the view is iterated. This means you can compose Arbitrarily many adaptors without paying any cost until you actually consume the elements.

Theorem. For a range adaptor pipeline source | views::filter(pred) | views::transform(f) | views::take(n)No element evaluation occurs At pipeline construction time; all computation is deferred to iteration.

Proof. We reason about the implementation model mandated by the standard [N4950 §26.5.2].

  1. Each adaptor is a class template whose constructor stores references (or copies) to the source range and the callable. No iteration of the source occurs in the constructor. By [N4950 §26.5.2], std::ranges::view requires O(1)O(1) construction, which precludes iterating the source.

  2. views::filter stores the source range and the predicate. Its begin() returns an iterator that, on operator++Advances the source iterator past elements failing the predicate. The predicate is only invoked when the iterator is advanced, not at construction.

  3. views::transform stores the source and the function. Its iterator”s operator* applies f to the current element of the source iterator. The function f is invoked only when the element is dereferenced, not when the view is constructed.

  4. views::take(n) stores the source and a counter. Its begin() returns an iterator that increments the counter on each operator++ and compares it against n in operator!= with the sentinel. No elements are consumed at construction.

Since each adaptor’s constructor only captures its inputs (at O(1)O(1) cost), and each adaptor’s Iterator performs work only when advanced or dereferenced, the entire pipeline performs zero element Processing until iteration begins. QED.

#include <iostream>
#include <vector>
#include <ranges>
#include <string>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// This creates a LAZY pipeline — no computation yet [N4950 §26.5.2]
auto pipeline = numbers
| std::views::filter([](int x) { return x % 2 == 0; }) // {2,4,6,8,10}
| std::views::transform([](int x) { return x * x; }) // {4,16,36,64,100}
| std::views::take(3); // {4,16,36}
// Computation happens HERE during iteration
std::cout << "Result: ";
for (int x : pipeline) {
std::cout << x << " ";
}
// Output: Result: 4 16 36
std::cout << "\n";
}