Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 11 – Iterators, algorithms and ranges

C++ views and pipelines: filter, transform, iota

Build lazy pipelines with std::views::filter, transform, take and iota, store their results with std::ranges::to, and keep views from outliving their data.

  • Intermediate
  • 15 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 6 of 15 minutes free
Free: A pipeline runs nothing until a loop asks for a value · iota counts without a container, and ranges::to stores the results · Keep a view for one pass and a container for results you keep · Practise: lazy pipelines. With a pass: 2 in-depth parts and 3 interview answers, about 9 minutes.

What you will learn

  • Compose lazy pipelines with views::filter, views::transform, views::take and views::iota
  • Convert a view into a container with std::ranges::to
  • Explain when a view outlives its data, and how to prevent it
  • Choose between a view and a container for a task

Before you start

Over the numbers 1 to 8, the pipeline v | std::views::filter(is_even) | std::views::transform(square) | std::views::take(2) tested six numbers, squared two and printed 4 and 16, and building it ran nothing at all. A view: A range that does not own its elements: it refers to elements stored elsewhere or computes them, is cheap to copy, and does its work only while it is iterated, as std::views::filter and std::views::iota do. is a range that usually owns no elements: it refers to elements stored somewhere else, or computes them, and it does its work only when you iterate it.

C++ views combine with | into pipelines. Nothing runs until a loop asks for a value, and then the values come one at a time. std::views::take(n) stops after n values, std::views::iota(1, n + 1) produces the numbers 1 to n without storing them, and C++23’s std::ranges::to<std::vector>() runs a pipeline and stores what it produces. A view that refers to its data does not keep it alive, so it must not outlive the container it reads, the one rule that the compiler cannot always check for you.

A pipeline runs nothing until a loop asks for a value

The figure follows the first value through three views; the program below prints a line at every step:

A vector feeds filter, transform and take one element at a time: 1 is rejected, 2 passes, becomes 4 and reaches the loop.std::vector v1 2 3 4 5 6 7 8views::filter(is even)reads 1: no; reads 2: yesviews::transform(square)2 becomes 4views::take(2)the first of two valuesthe for loopprints got 4, then asks againone elementat a time244

How a lazy pipeline produces its first value

Text description of the diagram

The diagram follows the first value of the pipeline v | filter(is even) | transform(square) | take(2), from the bottom of the chain to the loop that asks for it.

  1. The vector v holds 1 to 8. Nothing has been computed when the loop starts.
  2. The filter reads one element at a time: 1 is not even, so it reads 2, which passes.
  3. The transform squares 2 and hands on 4.
  4. take(2) passes 4 on as the first of the two values it will deliver.
  5. The loop prints "got 4" and asks for the next value, and the same chain runs again from 3.
lazy_pipeline.cpp C++ · lazy_pipeline.cpp
// Views are lazy: building a pipeline runs nothing, and iterating it pulls one element at a time through every step.
#include <print>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> v{1, 2, 3, 4, 5, 6, 7, 8};

    auto pipeline = v | std::views::filter([](int x) { std::println("  filter {}", x); return x % 2 == 0; })
                      | std::views::transform([](int x) { std::println("  square {}", x); return x * x; })
                      | std::views::take(2);
    std::println("pipeline built; nothing has run yet");

    for (int y : pipeline) std::println("got {}", y);
    std::println("done");
}
What runs, and when, in a pipeline of three views Python · lazy_pipeline_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra lazy_pipeline.cpp -o lazy_pipeline && ./lazy_pipeline
g++-16 -std=c++23 -Wall -Wextra lazy_pipeline.cpp -o lazy_pipeline-gcc && ./lazy_pipeline-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra lazy_pipeline.cpp -o lazy_pipeline && ./lazy_pipeline
pipeline built; nothing has run yet
  filter 1
  filter 2
  square 2
got 4
  filter 3
  filter 4
  square 4
got 16
  filter 5
  filter 6
done
$ g++-16 -std=c++23 -Wall -Wextra lazy_pipeline.cpp -o lazy_pipeline-gcc && ./lazy_pipeline-gcc
pipeline built; nothing has run yet
  filter 1
  filter 2
  square 2
got 4
  filter 3
  filter 4
  square 4
got 16
  filter 5
  filter 6
done

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lazy_pipeline_session.py

Building pipeline printed nothing: the three range adaptor: A function object in std::views, such as filter, transform or take, that takes a range and returns a view of it. Adaptors combine with | into a pipeline: v | std::views::filter(is_even) | std::views::transform(square). only stored the functions they were given and a reference to v. The loop then pulled the values one at a time. Step through the run, one request or one value at a time:

Interactive · stage by stage How the loop pulls two values through three views Step through lazy_pipeline.cpp. Each step is one request or one value moving between two neighbours of the chain, with the line the program printed.
How the loop pulls two values through three views, step 9 of 9. Showing for (int y : pipeline) (printed done), views::take(2) (delivered 2 of 2), views::transform(square) (4 becomes 16), views::filter(is even) (read 5 and 6: found 6), std::vector v (7 and 8 never read). In focus: views::take(2), for (int y : pipeline).for (int y : pipeline)printed doneviews::take(2)delivered 2 of 2views::transform(square)4 becomes 16views::filter(is even)read 5 and 6: found 6std::vector v7 and 8 never read

The drawing shows the whole scene; its 9 steps are listed below.

Every step as text
  1. Built, and nothing ran. Building pipeline stored the two lambdas, the 2 of take and a reference to v. The program printed pipeline built; nothing has run yet, and not one element had been read.
  2. The loop asks. The for loop starts by asking take for its first value. take has none of its own, so it passes the request down the chain.
  3. Down to the filter. take asks transform, and transform asks filter, because every view only computes from the one below it.
  4. filter reads 1, then 2. The filter reads 1 from v and rejects it (filter 1), reads 2 and keeps it (filter 2). It stops there: one value is all that was asked for.
  5. 2 is squared. The filter hands 2 up, and the transform squares it (square 2): 4.
  6. got 4. take passes 4 on as the first of its two values, and the loop prints got 4. Only now has anything reached the loop.
  7. The second value. The loop asks again. The filter reads 3 and rejects it, reads 4 and keeps it; the transform squares 4, and the loop prints got 16.
  8. One more step forward. After got 16 the loop moves its iterator on once more, and that makes the filter look for the next even number: it reads 5 and 6 (filter 5, filter 6). No square 6 follows, because nobody reads the value.
  9. take says done. take has delivered its two values, so the position after them is its end, and the loop stops: done. 7 and 8 were never read, and no intermediate vector existed at any point.

For the first value, the filter read 1, rejected it, read 2 and passed it on; the transform squared it; and only then did the loop print 4. After the second value, the loop still incremented its iterator once more, and that increment made the filter look for the next even number, so it tested 5 and 6 before take reported that it had delivered its two values. Elements 7 and 8 were never read, and no intermediate vector existed at any point. That is what lazy evaluation: Doing work only when its result is asked for. A view computes an element when a loop or an algorithm asks for the next one, so building a pipeline runs nothing, and elements that nobody asks for are never computed. means for C++ views: each step does the least work that the next one asks for.

The | is only notation. std::views::filter(v, is_even) is the same view as v | std::views::filter(is_even); the pipe reads better when several steps follow one another. A pipeline without its data is an object too: auto evens_squared = std::views::filter(is_even) | std::views::transform(square); can be applied later to any range, as v | evens_squared, which lets you name a sequence of steps once and reuse it.

Checkpoint

Check: How a lazy pipeline runs

  1. Question 1 of 2 In auto p = v | std::views::filter(pred);, when does pred run?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Only while p is being iterated, one element at a time

    Views are lazy. Creating p stores pred and a reference to v and computes nothing; pred runs when a loop or an algorithm asks p for its next element, so a pipeline that is never iterated never calls it.

  2. Question 2 of 2 In the run of lazy_pipeline.cpp over 1 to 8, how many times did the filter's lambda run?

    Type a number.

    A hint

    Count the lines that start with filter, up to done.

    Show the answer: Question 2 of 2

    Answer: 6 times

    filter 1 to filter 6: four tests for the two values that reached the loop, and two more, 5 and 6, for the last step of the loop's iterator. 7 and 8 were never read.

iota counts without a container, and ranges::to stores the results

Numbers from iota, filtered, squared and stored:

even_squares.cpp C++ · even_squares.cpp
// A pipeline over numbers that iota generates, turned into a vector with std::ranges::to (C++23).
#include <print>
#include <ranges>
#include <vector>

int main() {
    // iota(1, 11) counts 1, 2, ..., 10 without storing them: the loop counter as a range.
    auto even_squares = std::views::iota(1, 11)
                      | std::views::filter([](int x) { return x % 2 == 0; })
                      | std::views::transform([](int x) { return x * x; });

    std::vector<int> stored = even_squares | std::ranges::to<std::vector>();
    std::println("even squares up to 10: {}", stored);

    // A view is cheap to keep and can be iterated again; each time, it computes the values afresh.
    int sum = 0;
    for (int x : even_squares) sum += x;
    std::println("their sum: {}", sum);

    // An unbounded iota with take: the first five multiples of 7.
    auto sevens = std::views::iota(1) | std::views::transform([](int x) { return 7 * x; }) | std::views::take(5);
    std::println("multiples of 7: {}", sevens | std::ranges::to<std::vector>());
}
Even squares, their sum and the first multiples of 7 Python · even_squares_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra even_squares.cpp -o even_squares && ./even_squares
g++-16 -std=c++23 -Wall -Wextra even_squares.cpp -o even_squares-gcc && ./even_squares-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra even_squares.cpp -o even_squares && ./even_squares
even squares up to 10: [4, 16, 36, 64, 100]
their sum: 220
multiples of 7: [7, 14, 21, 28, 35]
$ g++-16 -std=c++23 -Wall -Wextra even_squares.cpp -o even_squares-gcc && ./even_squares-gcc
even squares up to 10: [4, 16, 36, 64, 100]
their sum: 220
multiples of 7: [7, 14, 21, 28, 35]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 even_squares_session.py

std::views::iota(1, 11) is the half-open range from 1 to 10, the counter of for (int i = 1; i < 11; ++i) as a range. std::views::iota(1) has no end and counts for as long as anyone asks, which take(5) limits to five values. std::ranges::to<std::vector>() (C++23, paper P1206R7) runs a pipeline to its end and builds a vector, deducing the element type, and it worked with both standard libraries. The pipeline even_squares was iterated twice, and each pass computed the values again: a view stores the recipe, not the results. Store the results in a container when you need them more than once and the computation is not cheap.

Other adaptors of C++20 follow the same pattern. std::views::reverse walks a range backwards, std::views::drop(n) skips the first n elements, std::views::take_while(pred) stops at the first element that fails a test, and std::views::keys and std::views::values pick the two halves of the entries of a map.

Checkpoint

Check: iota and ranges::to

  1. Question 1 of 2 What does std::views::iota(1, 6) contain?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 1, 2, 3, 4, 5

    iota(first, last) counts from first up to, but not including, last: the same half-open range as a loop for (int i = 1; i < 6; ++i). iota(1) with one argument never ends, so it needs take or another limit.

  2. Question 2 of 2 What does std::views::iota(3) | std::views::transform([](int x) { return x * x; }) | std::views::take(3) produce?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 9, 16, 25

    An unbounded iota is safe as long as something ends the pipeline: here take(3) stops after the third square, so only 3, 4 and 5 are ever computed.

Keep a view for one pass and a container for results you keep

Three questions about the result decide between them:

Interactive · answer and compare Keep a view, or store a container? Answer three questions about the result you need. An option that does not fit gets a caveat with the reason.
Will the result be returned from a function, stored, or shared between threads?
Will the result be read many times, with steps that cost something?
Is it read once, in the place where its source data lives?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

A view and a container compared on ownership, cost, when the work happens, lifetime and rereading
Keep Owns its elements Cost to create When the work happens May outlive its source Read several times
A view Mixed usually not: it refers to them or computes them Good constant: a few functions and references Mixed each time it is iterated Poor no, unless it owns the source Poor computes the values again
A container Good yes Poor an allocation and a copy of every element Good once, while it is filled Good yes Good reads the stored values
How the answers decide
  • “Will the result be returned from a function, stored, or shared between threads?” Yes, it is kept: adds a caveat to A view. A view refers to its source, so a view of a local container dangles once the function returns; return a container unless the view owns its source.
  • “Will the result be read many times, with steps that cost something?” Yes, several costly passes: adds a caveat to A view. A view stores the recipe, not the results, so every pass computes the values again; store them once.
  • “Is it read once, in the place where its source data lives?” Yes, one pass, here: adds a caveat to A container. Filling a container allocates and copies every element, work that a single pass over the source does not need.

Pipelines shine when the data is read once, in one place, while the container it comes from is alive: reports, searches and conversions written as a sentence of steps instead of a loop with temporary vectors. A container is the better choice for results that are kept, returned from a function, shared between threads or read many times.

Views are cheap because they skip the bookkeeping that containers do, and each skipped step has a rule:

  • A view that outlives its data. A view returned from a function must not refer to a local container, which is destroyed when the function returns: it would be a dangling view: A std::string_view, or another view that owns nothing, whose characters have been destroyed or freed. Reading it is undefined behaviour, which AddressSanitizer reports as a heap-use-after-free when the characters were on the heap. More in “std::string and std::string_view”.
  • A remembered first match. A filter view finds its first match once and remembers it, so it cannot be iterated through a const reference.
  • Double work. A transform placed before a filter runs twice for every element that passes the filter. C++26’s std::views::cache_latest, in GCC 16 but not yet in Apple clang 21, keeps the last value, so the transform runs once.
  • Two passes in ranges::to. Building a vector from a filter view walks the pipeline twice, once to count and once to copy.

Checkpoint

Check: A view or a container

  1. Question 1 of 2 A function builds a local std::vector<int> marks and returns marks | std::views::filter(passed). What is wrong?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The view refers to marks, which is destroyed when the function returns

    Return a container, such as the pipeline stored with std::ranges::to<std::vector>(), when the result leaves the function; keep views for data that outlives them.

  2. Question 2 of 2 Why can a filter view not be iterated through a const reference?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It finds its first match once and remembers it inside the view

    A filter view stores the position of its first match inside itself, so reading it for the first time changes the view, and a function that takes the view by const reference cannot iterate it.

Practise: lazy pipelines

The first question comes from the idea your answers show is weakest, and every choice explains what it gets right or wrong.

Practice that adapts to you

Practise this lesson

8 questions about 8 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 8 Which expression is the same view as v | std::views::filter(is_even)?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: std::views::filter(v, is_even)

    An adaptor can be called with the range or piped from it; the pipe reads better when several steps follow one another.

  2. Question 2 of 8 Why did lazy_pipeline.cpp never print filter 7 or filter 8?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: take(2) had delivered its two values, so the loop stopped asking

    Each value is pulled through the chain on request. Once take reported its end, the loop ended, and the rest of the vector was never touched.

  3. Question 3 of 8 auto evens_squared = std::views::filter(is_even) | std::views::transform(square); has no range in it. What is it?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: A pipeline without data, which can be applied later as v | evens_squared

    Combining adaptors without a range gives a reusable pipeline: the same steps can be applied to v, to another vector or to an iota range.

  4. Question 4 of 8 Which C++23 expression runs the pipeline pipe and stores its results in a std::vector?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: pipe | std::ranges::to<std::vector>()

    std::ranges::to (paper P1206R7) builds any container from a range and deduces the element type when given only the container template. A vector has no constructor that takes an arbitrary view directly, and views::all just makes a view of a range.

  5. Question 5 of 8 even_squares is iterated a second time, by the loop that adds up 220. What happens to the filter and the transform?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: They run again for the second pass, because a view stores the recipe, not the results

    A view does its work while it is iterated, every time. Store the results in a container when you need them more than once and the steps are not cheap.

  6. Question 6 of 8 What does creating v | std::views::filter(pred) | std::views::transform(f) cost, for a vector of a million elements?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: A constant, small amount, the same for any size of v

    A view costs a constant amount to create, whatever the data, which is why it can be built freely and passed around by value.

  7. Question 7 of 8 In lines | std::views::transform(parse) | std::views::filter(big), for which lines does parse run twice?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: For every line that passes the filter

    The list of what can go wrong says it: a transform placed before a filter runs twice for every element that passes the filter.

  8. Question 8 of 8 Several threads read the even squares of a large vector many times. What should they share?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: A container filled once with the results

    A container is the better choice for results that are kept, returned from a function, shared between threads or read many times; keep views for one pass where the data lives.

Views that outlive their data, remembered matches and double work, reproduced Pro: opens with a Pro, Premium or Ultimate pass

Each rule of the previous list comes from a failure that compiles and often appears to work. This part reproduces each one on both standard libraries, under AddressSanitizer where memory is involved, counts the extra work, and shows the fix that holds.

In this part:

  • A view of a destroyed vector, caught by AddressSanitizer
  • Why a filter view cannot be iterated through const, and what its memory misses
  • Counted: a transform before a filter runs twice
  • C++26 preview: views::cache_latest, already in GCC 16

8 runnable examples (C++, Python) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Pipelines in practice: ranges::to, measured costs and a report to build Pro: opens with a Pro, Premium or Ultimate pass

A pipeline is usually the start of a result that someone keeps, so the step into a container and the cost of the whole chain matter as much as the views themselves. This part measures both on the two libraries, sets out when to keep a view and when to keep a container, and ends with a report to build from views.

In this part:

  • What ranges::to does with sized and filtered views
  • Measured: a pipeline against a hand-written loop, optimised and not
  • When to keep a view and when to keep a container
  • Your turn: a report built from views

9 runnable examples (C header, C++, Python) · about 6 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher · Warm-up

    What does it mean that std::views::filter is lazy, and when does its predicate run?

    Lazy means that creating the view computes nothing: v | std::views::filter(pred) only stores pred and a reference to v. The predicate runs while the view is iterated, one element at a time, whenever a loop or an algorithm asks for the next element, so a view that is never iterated never calls it. The work happens again each time the view is iterated, using the data as it is at that moment. In a chain that ends with take(n), laziness also means that reading stops soon after n values have been produced, so the rest of the data is never touched. A good answer adds the consequence for lifetimes: since the view only refers to v, v must still exist whenever the view is iterated.

  2. Mid to senior

    A function returns auto, computed as items | std::views::filter(is_active), where items is a local std::vector. The tests pass on one machine and crash on another, and AddressSanitizer reports stack-use-after-return. Explain, and give three fixes with their trade-offs.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A log pipeline, lines | views::transform(parse_record) | views::filter(is_error), is slower than expected, and a profiler shows parse_record running about twice for every error line. Explain why, and how you would restructure it.

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    A colleague proposes a class that stores a std::ranges::filter_view of one of its own std::vector members and returns it from a const member function. What problems do you expect, and what design would you suggest instead?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A view refers to data or computes it, and does its work only while it is iterated, one element at a time.
  • | builds pipelines from adaptors such as views::filter, views::transform, views::take and views::iota.
  • std::ranges::to<std::vector>() (C++23) runs a pipeline and stores the results.
  • A view must not outlive the container it refers to; return containers, not views of locals.

Terms in this lesson

lazy evaluation
Doing work only when its result is asked for. A view computes an element when a loop or an algorithm asks for the next one, so building a pipeline runs nothing, and elements that nobody asks for are never computed.
range adaptor
A function object in std::views, such as filter, transform or take, that takes a range and returns a view of it. Adaptors combine with | into a pipeline: v | std::views::filter(is_even) | std::views::transform(square).
view
A range that does not own its elements: it refers to elements stored elsewhere or computes them, is cheap to copy, and does its work only while it is iterated, as std::views::filter and std::views::iota do.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.