Ranges & Views — Complete Reference
"C++20 ranges and views: range concepts, lazy view adaptors, composable pipelines, projections, and the pitfalls that bite experienced engineers."
Ranges & Viewssince C++20A range is any type providing begin()/end(); a view is a lightweight, non-owning range adaptor with O(1) copy/move that transforms or filters elements lazily without materializing intermediate containers.
Overview
The <ranges> header (C++20) unifies three previously separate concerns:
- Range concepts — a constraint hierarchy that replaces raw iterator pairs in algorithm signatures.
- Range algorithms — all classic
<algorithm>functions restated to accept ranges directly, with projection support. - View adaptors — lazy, composable transformations in
std::views::that chain with|.
Range concept hierarchy
| Concept | Additional guarantee over parent |
|---|---|
std::ranges::range | begin() + end() |
std::ranges::input_range | single-pass iteration |
std::ranges::forward_range | multi-pass, equality-comparable iterators |
std::ranges::bidirectional_range | operator-- |
std::ranges::random_access_range | O(1) operator[] and iterator arithmetic |
std::ranges::contiguous_range | elements in contiguous memory |
std::ranges::sized_range | O(1) size() |
std::ranges::common_range | begin() and end() have the same type |
std::ranges::borrowed_range | iterators remain valid after the range is destroyed |
std::ranges::view | O(1) copy/move/destroy |
These distinctions are load-bearing: std::views::reverse requires bidirectional_range; random_access_range enables operator[] on the adapted result; borrowed_range determines whether an algorithm can safely return an iterator into a range you passed by value.
// C++20
static_assert(std::ranges::contiguous_range<std::vector<int>>);
static_assert(std::ranges::bidirectional_range<std::list<int>>);
static_assert(!std::ranges::random_access_range<std::list<int>>);
template<std::ranges::forward_range R>
void process(R&& r) { /* guaranteed multi-pass */ }Sentinel types
C++20 decouples the end-of-range sentinel from the iterator type. Begin and end no longer need to share a type — enabling null-terminated strings, infinite generators, and protocol-delimited streams:
// C++20 — heterogeneous begin/end types
struct NullSentinel {};
struct CharPtr {
const char* p;
using difference_type = std::ptrdiff_t;
using value_type = char;
using iterator_category = std::input_iterator_tag;
char operator*() const { return *p; }
CharPtr& operator++() { ++p; return *this; }
bool operator==(NullSentinel) const { return *p == '\0'; }
};
const char* cstr = "hello";
auto r = std::ranges::subrange(CharPtr{cstr}, NullSentinel{});
for (char c : r) std::print("{}", c); // hellostd::default_sentinel_t is the standard tag for this pattern — use it in your own types instead of inventing a new sentinel.
Syntax
Pipe operator and range adaptor objects
std::views::filter, std::views::transform, and all other adaptors are range adaptor objects — callable objects, not function templates. The | operator passes the left-hand range as the first argument:
// These are equivalent — C++20
auto a = std::views::filter(v, pred);
auto b = v | std::views::filter(pred);
// Partial application: adaptor minus the range produces a closure
auto drop3 = std::views::drop(3); // closure, not a view
auto result = v | drop3; // applied later
// Closures compose
auto pipeline = std::views::filter(is_even) | std::views::transform(square);
auto r = v | pipeline; // equivalent to v | filter(...) | transform(...)Examples
Core view adaptors (C++20)
#include <ranges>
#include <vector>
std::vector v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto evens = v | std::views::filter([](int n) { return n % 2 == 0; }); // C++20
auto squares = v | std::views::transform([](int n) { return n * n; }); // C++20
auto first5 = v | std::views::take(5); // C++20
auto skip3 = v | std::views::drop(3); // C++20
auto rev = v | std::views::reverse; // requires bidirectional_range // C++20
// Composable — nothing materialised until iterated
auto result = v
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::take(3);
// Lazily yields: 4, 16, 36Generating views (C++20)
// iota — arithmetic sequence, bounded or infinite
for (int i : std::views::iota(1, 6)) std::print("{} ", i); // 1 2 3 4 5
auto naturals = std::views::iota(0); // infinite
auto first10 = naturals | std::views::take(10); // safe — lazy
// counted — raw pointer or iterator + count
int arr[] = {10, 20, 30, 40, 50};
auto slice = std::views::counted(arr, 3); // {10, 20, 30}
// single / empty
auto one = std::views::single(42); // range of one element
auto nil = std::views::empty<int>; // empty typed rangeTuple-element views (C++20)
std::map<std::string, int> scores{{"Alice",95},{"Bob",82}};
auto names = scores | std::views::keys; // C++20
auto values = scores | std::views::values; // C++20
// elements<N> — Nth field of any tuple-like element (C++20)
std::vector<std::tuple<int, std::string, double>> rows{
{1, "alpha", 3.14}, {2, "beta", 2.71}
};
auto ids = rows | std::views::elements<0>; // int columnFlattening and splitting (C++20)
std::vector<std::vector<int>> nested{{1,2},{3,4},{5,6}};
for (int n : nested | std::views::join) // C++20
std::print("{} ", n); // 1 2 3 4 5 6
// join_with — insert delimiter between inner ranges (C++23)
for (int n : nested | std::views::join_with(0))
std::print("{} ", n); // 1 2 0 3 4 0 5 6
// split — tokenize on delimiter (C++20)
// Note: each element is a subrange<iterator>, not a string_view — see Pitfalls
std::string_view csv = "2024,01,15";
for (auto part : csv | std::views::split(','))
std::println("{}", std::string_view{part.begin(), part.end()});C++23 view adaptors
// enumerate — (index, element) pairs
for (auto [i, name] : std::views::enumerate(names)) // C++23
std::println("{}: {}", i, name);
// zip — multiple ranges in lockstep
for (auto [n, s] : std::views::zip(ints, strings)) // C++23
std::println("{} = {}", n, s);
// chunk — non-overlapping fixed-size windows
for (auto w : std::views::iota(1,8) | std::views::chunk(3)) // C++23
std::println("{}", w); // [1,2,3], [4,5,6], [7]
// slide — overlapping windows
for (auto w : std::views::iota(1,6) | std::views::slide(3)) // C++23
std::println("{}", w); // [1,2,3], [2,3,4], [3,4,5]
// stride — every Nth element
for (int n : std::views::iota(0,10) | std::views::stride(3)) // C++23
std::print("{} ", n); // 0 3 6 9
// cartesian_product — all N-ary combinations
for (auto [a, b] : std::views::cartesian_product( // C++23
std::views::iota(0,3), std::views::iota(0,2)))
std::print("({},{}) ", a, b); // (0,0)(0,1)(1,0)(1,1)(2,0)(2,1)
// repeat — infinite or bounded repetition
auto fives = std::views::repeat(5) | std::views::take(4); // C++23Range algorithms with projections (C++20)
Projections apply a function before the comparator, eliminating wrapper lambdas:
struct Employee { std::string name; int salary; int dept; };
std::vector<Employee> staff{
{"Alice", 95000, 2}, {"Bob", 72000, 1}, {"Carol", 88000, 2}
};
std::ranges::sort(staff, std::greater{}, &Employee::salary); // by salary desc
auto it = std::ranges::find(staff, 2, &Employee::dept); // first in dept 2
long high = std::ranges::count_if(staff,
[](int s){ return s > 80000; }, &Employee::salary);
auto lowest = std::ranges::min_element(staff, {}, &Employee::salary);Every std::ranges:: algorithm accepts a projection as a trailing argument — sort, find, min_element, unique, partition, and more.
Collecting into containers (C++23) and fold (C++23)
// std::ranges::to — C++23
auto squares = std::views::iota(1, 6)
| std::views::transform([](int n){ return n * n; })
| std::ranges::to<std::vector>(); // {1, 4, 9, 16, 25}
// Pre-C++23 workaround
auto r = std::views::iota(1, 6) | std::views::transform([](int n){ return n*n; });
std::vector<int> v(r.begin(), r.end());
// std::ranges::fold_left — replaces std::accumulate (C++23)
auto sum = std::ranges::fold_left(
std::views::iota(1, 6), 0, std::plus{}); // 15
// fold_left_first — no initial value, returns optional (C++23)
auto product = std::ranges::fold_left_first(
std::vector{2, 3, 4}, std::multiplies{}); // optional{24}Implementing a custom view (C++20)
Inherit from std::ranges::view_interface<Derived> to get empty(), front(), back(), operator[], and size() automatically derived from begin()/end():
struct FibView : std::ranges::view_interface<FibView> { // C++20
int limit;
explicit FibView(int n) : limit(n) {}
struct Sentinel {};
struct Iterator {
int a = 0, b = 1, limit;
using difference_type = std::ptrdiff_t;
using value_type = int;
using iterator_category = std::input_iterator_tag;
int operator*() const { return a; }
Iterator& operator++() { int t=a+b; a=b; b=t; return *this; }
Iterator operator++(int) { auto tmp=*this; ++*this; return tmp; }
bool operator==(Sentinel) const { return a > limit; }
};
Iterator begin() const { return {.limit=limit}; }
Sentinel end() const { return {}; }
};
static_assert(std::ranges::input_range<FibView>);
static_assert(std::ranges::view<FibView>);
// Composes with all view adaptors
auto big = FibView{1000}
| std::views::filter([](int n){ return n > 10; })
| std::ranges::to<std::vector>(); // C++23Best Practices
Prefer projections over a transform-then-compare pipeline. ranges::sort(v, {}, &T::field) avoids a wrapping view and a heap allocation.
Use auto&& in range-for over view pipelines. Views such as zip_view return proxy references (tuples of references), and auto [k, v] by value copies instead of binding:
// Correct — binds references through proxy
for (auto&& [k, v] : map | std::views::filter(/* ... */)) { k; v; }Materialize when you iterate more than once. filter_view re-runs the predicate on every pass. For expensive predicates over a range you traverse repeatedly, collect first:
auto filtered = data
| std::views::filter(expensive_check)
| std::ranges::to<std::vector>(); // C++23 — pay once, iterate freelyUse std::ranges::subrange to wrap iterator pairs for passing into range-aware APIs when you have legacy begin/end iterators from a non-range API.
Common Pitfalls
filter_view and reverse_view are not const-iterable
filter_view caches its begin() result on first call (for amortized O(1) start). The cache write makes begin() non-const, so const-qualifying the view breaks iteration:
const auto view = data | std::views::filter(pred);
for (int n : view) { } // compile error — begin() is not const
auto view2 = data | std::views::filter(pred); // mutable — OKThis also prevents passing filter_view to any function taking const Range& that then iterates it.
View invalidation after container mutation
A stored view becomes dangling if the underlying container invalidates its iterators. With std::vector, any push_back that triggers reallocation invalidates all iterators held by views into it:
std::vector<int> v{1,2,3,4,5};
auto view = v | std::views::drop(2); // holds iterators into v
v.push_back(6); // may reallocate
for (int n : view) { } // UB if reallocation occurredThe fix: either re-compute the view after mutation, or use reserve() to prevent reallocation.
Dangling iterators from temporaries
std::ranges algorithms detect rvalue non-borrowed_range arguments and return std::ranges::dangling — a type that does not dereference — instead of an iterator:
auto it = std::ranges::find(std::vector{1,2,3}, 2);
*it; // compile error — std::ranges::dangling has no operator*
std::vector v{1,2,3};
auto it2 = std::ranges::find(v, 2); // OK — lvalue is borrowed implicitlystd::string_view, std::span, and std::ranges::subrange are borrowed_ranges; their iterators are always safe to return.
split_view elements are subranges, not string_views
Each element produced by views::split is a subrange<iterator>, not a string_view. You must construct explicitly:
for (auto part : sv | std::views::split(',')) {
// C++20: manual construction
auto token = std::string_view{part.begin(), part.end()};
// C++23: range constructor
auto token2 = std::string_view{part};
}Infinite views passed to non-terminating algorithms
There is no compile-time guard against passing an infinite view to an algorithm that doesn't terminate on such input:
// HANGS — iota(0) is infinite; -1 never appears
auto it = std::ranges::find(std::views::iota(0), -1);
// Safe — bound with take
auto it2 = std::ranges::find(
std::views::iota(0) | std::views::take(10'000), -1);See Also
std::span—borrowed_rangeview over contiguous memory (C++20)std::string_view—borrowed_rangeover character sequences (C++17)std::ranges::subrange— wraps a begin/sentinel pair into a range (C++20)std::generator— coroutine-based lazy range source (C++23)<algorithm>— all standard algorithms havestd::ranges::equivalents (C++20)