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?
- 2. Range Algorithms (
std::ranges::*) - 3. Views
- 4. Range and View Concepts
- 5.
std::span - 6. C++23 Additions
- 7. When to Use Ranges
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 instd::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:
- Cheap multiple views over same data — no intermediates built.
- Infinite ranges usable —
views::iota(0) | views::take(5)={0, 1, 2, 3, 4}. - Side effects in
transform/filtercallables — run during iteration, not pipeline construction. - Each iteration re-runs the pipeline — iterating a
transform_viewtwice → transform called twice per element. Materialize withranges::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.
std::ranges::range— hasbegin()andend().std::ranges::sized_range— knows size in O(1).std::ranges::input_range/forward_range/bidirectional_range/random_access_range/contiguous_range— match iterator categories.std::ranges::view— non-owning, cheap to copy. Implementsview_interface.std::ranges::common_range—begin()+end()same type (compat with classic STL iterator-pair algorithms).std::ranges::borrowed_range— safe to use iterators after range expression goes out of scope.std::ranges::viewable_range— convertible to a view (viaviews::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:
- Dynamic extent —
std::span<T>(orstd::span<T, std::dynamic_extent>). Length = runtime. - Static extent —
std::span<T, N>. Length = compile-time const. One pointer wide instead of two.
std::span vs ranges views:
std::span<T>— contiguous + bounded. Fancy(T*, size_t).- Ranges views — lazy, may be non-contiguous (e.g.,
transform_view). std::spanfor "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 neededviews::commonfirst).
6.2 New Views
views::chunk(n)— split into chunks ofn. Last may be smaller.views::slide(n)— sliding window of sizen.views::chunk_by(pred)— split wherepred(prev, curr)is false.views::stride(n)— everynth element.views::adjacent<N>— fixed compile-time sliding window, returning tuples.views::zip(r1, r2, ...)— element-wise tuple of multiple ranges.views::zip_transform(fn, r1, r2, ...)—zip+ applyfnto each tuple.views::enumerate— pairs(index, element).views::cartesian_product(r1, r2, ...)— Cartesian product as tuples.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_yieldvalues 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 likestd::accumulate, cleaner signatures.- Concept-constrained, accumulator type deduced.
_first/_lastvariants make initial value optional, returnstd::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
- Default to ranges for new C++20+ code — more readable, intent explicit.
- Views for transformation pipelines — avoid materializing intermediate
vectors; pipe through, materialize at end withranges::to(C++23) or pass straight to consumer. std::spanfor non-owning array params — replaces(T*, size_t)andT[]in APIs.- View lifetime — views borrow from underlying range; must not outlive it. Storing a view as class member usually a bug (use
borrowed_rangeconstraints). - Iterator fallback OK — fine control, C++17 compat, or when ranges lib doesn't have the adaptor.
- Avoid
filter_viewfor multiple iterations — re-scans from beginning eachbegin()call. Materialize first.