C++ Ranges, Views, and Span


  • Description: A note on C++20 Ranges and views composition, range concepts, projections, std::span, and the C++23 additions (ranges::to, chunk, zip, std::generator)
  • My Notion Note ID: K2A-B1-16
  • Created: 2021-06-15
  • Updated: 2026-02-28
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. What Is a Range?

  • Range = anything iterable. Informally, a pair of iterators (begin() / end()) or anything yielding one.
  • C++20 <ranges> builds range-aware algorithms + lazy adaptors on top.
#include <ranges>
#include <vector>

std::vector<int> v = {1, 2, 3};
// v is a range — std::ranges::sort(v) works directly without v.begin()/v.end()
  • Ranges replace most begin/end pair usage in modern C++.
  • Enable lazy view pipelines (§3) + unified algorithm signatures (§2).

2. Range Algorithms (std::ranges::*)

  • Nearly every <algorithm> and <numeric> algorithm has a range overload in std::ranges.

2.1 Algorithm Overloads

#include <ranges>
#include <algorithm>
#include <vector>

std::vector<int> v = {3, 1, 4, 1, 5};

std::ranges::sort(v);                                // takes a whole range
auto it = std::ranges::find(v, 5);                   // search whole range
bool any_neg = std::ranges::any_of(v,
                   [](int x){ return x < 0; });
auto [mn, mx] = std::ranges::minmax(v);              // pair, structured-binding-friendly
  • Same algorithms, single range arg. Enforce range concepts → clearer errors than iterator-pair forms.

2.2 Projections

  • Most range algorithms take optional projection — callable extracting a key from each element before comparison. Replaces custom lambdas for member-comparison.
struct Person {
    std::string name;
    int         age;
};

std::vector<Person> people = { /* ... */ };

// Sort by age ascending — no comparator needed, just a projection
std::ranges::sort(people, {}, &Person::age);

// Find the oldest
auto oldest = std::ranges::max_element(people, {}, &Person::age);

// Custom comparator AND projection
std::ranges::sort(people, std::greater<>{}, &Person::age);   // age descending
  • Signature: (range, comparator, projection). {} = default comparator (std::ranges::less).
  • Projection = any callable: member pointer (&Person::age), member fn pointer, free fn, lambda.

3. Views

  • View = lightweight, non-owning, lazy range adaptor. No allocation, no storage. Wraps an underlying range, produces values on demand.

3.1 Common Views

View What it does
views::filter(pred) Keep elements where pred(x) is true
views::transform(fn) Apply fn to each element
views::take(n) First n elements
views::drop(n) Skip first n elements
views::take_while(pred) Until pred is false
views::drop_while(pred) Skip until pred is false
views::reverse Iterate in reverse
views::iota(n) Infinite sequence n, n+1, n+2, …
views::iota(a, b) Bounded sequence a, a+1, ..., b-1
views::keys / views::values Project pair::first / pair::second (e.g. for maps)
views::join Flatten a range of ranges
views::split(delim) Split a range by a delimiter
views::elements<N> Project the Nth element of each tuple
views::common Force begin() and end() to have the same type (for legacy algorithms)
views::all Wrap a range in a view (the conversion that | does implicitly)

3.2 View Composition with |

  • Pipe | builds a lazy pipeline.
#include <ranges>
#include <vector>
#include <iostream>

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

auto pipeline = v
    | std::views::filter([](int x) { return x % 2 == 0; })
    | std::views::transform([](int x) { return x * x; })
    | std::views::take(3);

for (int x : pipeline) std::cout << x << " ";
// Output: 4 16 36
  • Pipe is sugar — v | views::filter(p)views::filter(v, p). Use whichever reads better.

3.3 Lazy Evaluation

  • Views compute nothing until iteration. Pulls one element at a time through the chain. No intermediate containers.

Implications:

  1. Cheap multiple views over same data — no intermediates built.
  2. Infinite ranges usableviews::iota(0) | views::take(5) = {0, 1, 2, 3, 4}.
  3. Side effects in transform/filter callables — run during iteration, not pipeline construction.
  4. Each iteration re-runs the pipeline — iterating a transform_view twice → transform called twice per element. Materialize with ranges::to<vector> (C++23) for repeated iteration.
auto squares_under_100 = std::views::iota(1)
    | std::views::transform([](int x) { return x * x; })
    | std::views::take_while([](int x) { return x < 100; });

for (int x : squares_under_100) std::cout << x << " ";
// 1 4 9 16 25 36 49 64 81

4. Range and View Concepts

  • <ranges> defines hierarchy of concepts constraining ranges. Show up in errors + custom range adaptors.
  1. std::ranges::range — has begin() and end().
  2. std::ranges::sized_range — knows size in O(1).
  3. std::ranges::input_range / forward_range / bidirectional_range / random_access_range / contiguous_range — match iterator categories.
  4. std::ranges::view — non-owning, cheap to copy. Implements view_interface.
  5. std::ranges::common_rangebegin() + end() same type (compat with classic STL iterator-pair algorithms).
  6. std::ranges::borrowed_range — safe to use iterators after range expression goes out of scope.
  7. std::ranges::viewable_range — convertible to a view (via views::all).
  • Constrain function templates with these → sharp errors when callers pass wrong shape.

5. std::span

  • std::span<T> (C++20, <span>) — non-owning view of contiguous sequence. = T* + length.
  • Idiomatic way to write "view of an array" parameter without committing to a container type.
#include <span>
#include <vector>
#include <array>
#include <iostream>

void print(std::span<const int> s) {
    for (int x : s) std::cout << x << " ";
}

std::vector<int>     v = {1, 2, 3};
std::array<int, 3>   a = {4, 5, 6};
int                  raw[] = {7, 8, 9};

print(v);                  // works
print(a);                  // works
print(raw);                // works (decays via deduction guide)
print({v.data(), 2});      // explicit (T*, size_t): first 2 elements of v

2 flavors:

  1. Dynamic extentstd::span<T> (or std::span<T, std::dynamic_extent>). Length = runtime.
  2. Static extentstd::span<T, N>. Length = compile-time const. One pointer wide instead of two.

std::span vs ranges views:

  1. std::span<T>contiguous + bounded. Fancy (T*, size_t).
  2. Ranges views — lazy, may be non-contiguous (e.g., transform_view).
  3. std::span for "contiguous slice"; ranges views for "compose lazy operations."
  • Span is itself a range → pipe through views: print(v | std::views::take(3)).

6. C++23 Additions

6.1 std::ranges::to

  • Easy materialization of a range into a container.
#include <ranges>
#include <vector>

auto v = std::views::iota(1, 6)
       | std::views::transform([](int x) { return x * x; })
       | std::ranges::to<std::vector>();
// v is std::vector<int>{1, 4, 9, 16, 25}

// You can also specify the container type explicitly
auto m = std::views::zip(keys, values)
       | std::ranges::to<std::map<std::string, int>>();
  • Pre-C++23: std::vector<int>(rng.begin(), rng.end()) (often needed views::common first).

6.2 New Views

  1. views::chunk(n) — split into chunks of n. Last may be smaller.
  2. views::slide(n) — sliding window of size n.
  3. views::chunk_by(pred) — split where pred(prev, curr) is false.
  4. views::stride(n) — every nth element.
  5. views::adjacent<N> — fixed compile-time sliding window, returning tuples.
  6. views::zip(r1, r2, ...) — element-wise tuple of multiple ranges.
  7. views::zip_transform(fn, r1, r2, ...)zip + apply fn to each tuple.
  8. views::enumerate — pairs (index, element).
  9. views::cartesian_product(r1, r2, ...) — Cartesian product as tuples.
  10. views::repeat(value) / views::repeat(value, n) — infinite or bounded repetition.
std::vector<int>         a = {1, 2, 3};
std::vector<std::string> b = {"one", "two", "three"};

for (auto [n, s] : std::views::zip(a, b)) {
    std::cout << n << "=" << s << " ";
}
// 1=one 2=two 3=three

6.3 std::generator (Coroutine-Based Range)

  • std::generator<T> — coroutine return type. Easiest way to write your own range from scratch. co_yield values one at a time.
#include <generator>
#include <ranges>
#include <iostream>

std::generator<int> fibs() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        int next = a + b;
        a = b;
        b = next;
    }
}

for (int x : fibs() | std::views::take(10)) std::cout << x << " ";
// 0 1 1 2 3 5 8 13 21 34
  • Simpler than custom iterator class with begin(), end(), iterator type.
  • Natural for ranges that compute lazily but don't fit existing view adaptors.

6.4 Folds

  • std::ranges::fold_left / fold_right — reductions like std::accumulate, cleaner signatures.
  • Concept-constrained, accumulator type deduced. _first / _last variants make initial value optional, return std::optional.
#include <ranges>
#include <vector>

std::vector<int> v = {1, 2, 3, 4, 5};
int sum = std::ranges::fold_left(v, 0, std::plus{});           // 15
int product = std::ranges::fold_left(v, 1, std::multiplies{}); // 120

7. When to Use Ranges

  1. Default to ranges for new C++20+ code — more readable, intent explicit.
  2. Views for transformation pipelines — avoid materializing intermediate vectors; pipe through, materialize at end with ranges::to (C++23) or pass straight to consumer.
  3. std::span for non-owning array params — replaces (T*, size_t) and T[] in APIs.
  4. View lifetime — views borrow from underlying range; must not outlive it. Storing a view as class member usually a bug (use borrowed_range constraints).
  5. Iterator fallback OK — fine control, C++17 compat, or when ranges lib doesn't have the adaptor.
  6. Avoid filter_view for multiple iterations — re-scans from beginning each begin() call. Materialize first.