The Ranges Library (C++20)
For decades, STL algorithms meant typing v.begin(), v.end() on every call and nesting transformations inside-out. C++20 ranges change that. You pass a whole container directly, and you build readable, left-to-right pipelines with the pipe | operator. By the end of this lesson you'll sort with std::ranges::sort , compose lazy views like filter , transform , take , and iota , and understand why none of it allocates intermediate containers.
Learn The Ranges Library (C++20) in our free C++ course — an interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Think of a ranges pipeline as a factory conveyor belt . Each view adaptor — filter , transform , take — is a station the items pass through, in order. Crucially, an item isn't processed until it actually reaches a station (lazy evaluation), and there are no warehouses between stations stockpiling half-finished goods (no intermediate containers). The pipe | is just the belt connecting the stations in reading order, so data | filter | transform | take describes the journey exactly as it happens.
1. Ranges Algorithms: No More begin()/end()
Classic STL algorithms take a pair of iterators . It works, but the boilerplate adds up and it's easy to mismatch begin/end from different containers. Here's the familiar std::sort :
The std::ranges versions in take the whole range directly. The call shrinks to std::ranges::sort(v) — clearer, and impossible to pass mismatched iterators.
Your turn. Replace the blank with a single ranges call that sorts the whole vector:
2. Views: Lazy, Composable Pipelines
A view is a lightweight, non-owning range adaptor that evaluates lazily : it produces each element only when you ask for it. The standard adaptors live in std::views — filter keeps elements that match, transform maps each element, and take yields only the first n . You connect them with the pipe | operator, reading left to right.
Views can even generate data. std::views::iota produces a sequence of numbers on demand without any container behind it. Pair it with take to slice an otherwise unbounded sequence safely.
These lines should build a pipeline that keeps evens, doubles them, and takes two. Put them in the right order:
Start from the source (A), filter to evens (B), transform by doubling (C), then take the first two (D). The pipe order matches the data flow.
3. Projections: Sort by a Member, No Comparator
Many ranges algorithms accept an optional projection : a function applied to each element before the algorithm inspects it. Want to sort people by age? Pass &Person::age as the projection — no hand-written comparator needed. The signature is ranges::sort(range, comparator, projection) ; pass {' '} for the default comparator.
A naive approach to "filter then transform" builds a temporary vector after each step. Ranges avoid this because each view is lazy and non-owning : it stores only a reference to its source and the operation to apply. When you iterate the final view, it pulls one element through the entire chain before moving to the next — so no stage ever materialises a whole intermediate container.
That is also why views::take can sit on top of an unbounded views::iota — the pipeline simply stops pulling once it has enough.
1, 2, 3, 4 — it is half-open, so the upper bound 5 is excluded.
2 4 — keep evens (2, 4, 6), then take the first two.
std::ranges::sort(v) — it takes the whole range directly.
Build a single pipeline from views::iota that keeps odd numbers, cubes them, and takes the first three — all lazily, with no intermediate containers.
Practice quiz
What header provides the C++20 ranges library?
- <ranges>
- <views>
- <algorithm>
- <iterator>
Answer: <ranges>. The ranges library, including views and range algorithms, lives in the <ranges> header.
How do classic algorithms like std::sort specify the data to operate on?
- With an index and a length
- With a container reference only
- With a pair of iterators (begin, end)
- With a single range object
Answer: With a pair of iterators (begin, end). Classic STL algorithms take an iterator pair; ranges algorithms can take a whole range directly.
What does std::ranges::sort(v) accept that std::sort does not?
- A lambda by value only
- A whole range (the container itself) instead of two iterators
- A comparison only
- A C-style array length
Answer: A whole range (the container itself) instead of two iterators. std::ranges::sort(v) takes the range directly, so you no longer write v.begin(), v.end().
What is a view in the ranges library?
- A thread that processes the range
- A deep copy of the container
- A sorted snapshot of the data
- A lazy, non-owning adaptor over a range that computes elements on demand
Answer: A lazy, non-owning adaptor over a range that computes elements on demand. A view is a lightweight, non-owning range adaptor; it does not copy data and evaluates lazily.
What does 'lazy evaluation' mean for views?
- Elements are computed only when you iterate, not up front
- The view runs on a background thread
- The container is sorted lazily
- Nothing is ever computed
Answer: Elements are computed only when you iterate, not up front. A view does no work until iterated; each element is produced on demand as you walk it.
Which operator composes range adaptors into a pipeline?
- The double colon ::
- The pipe operator |
- The plus operator +
- The arrow operator ->
Answer: The pipe operator |. Range adaptors compose left-to-right with the pipe operator, e.g. v | filter(...) | transform(...).
What does std::views::iota(1, 5) produce?
- The numbers 1, 2, 3, 4, 5
- A single value 5
- A random sequence
- The numbers 1, 2, 3, 4
Answer: The numbers 1, 2, 3, 4. iota(1, 5) is a half-open range: it yields 1, 2, 3, 4 (5 is the past-the-end bound).
What does std::views::take(3) do in a pipeline?
- Sorts and keeps the top 3
- Skips the first 3 elements
- Yields only the first 3 elements of the range
- Repeats each element 3 times
Answer: Yields only the first 3 elements of the range. take(n) is a view that yields at most the first n elements of the range it adapts.
What is a projection in a ranges algorithm?
- A parallel execution policy
- A function applied to each element before the algorithm inspects it
- A way to render the range to screen
- A copy of the range
Answer: A function applied to each element before the algorithm inspects it. A projection transforms each element for comparison purposes, e.g. sorting people by their .age member.
Why can views::filter and views::transform be chained efficiently?
- They are lazy and non-owning, so no intermediate containers are created
- They run on the GPU
- They sort the data first
- They build one intermediate vector per stage
Answer: They are lazy and non-owning, so no intermediate containers are created. Because each view is lazy and non-owning, a pipeline streams elements through without allocating temporaries.