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.