Ranges & Views
In the last lesson you used std::ranges algorithms on whole containers. C++20 also introduced views: lightweight objects that present the elements of another range filtered, transformed, reversed or sliced, without copying anything. You can join views with the pipe symbol | into readable pipelines, much like Unix shell commands or Java streams.
In this lesson you will build pipelines with the most useful views, see that views are lazy, use the C++23 additions enumerate, zip and std::ranges::to, and learn the one big rule for using views safely.
Ranges and your first pipeline
A range is anything you can loop over with begin() and end(): vectors, arrays, strings, maps. A view is a range that does not own its elements; it looks at another range and adapts it. Views live in the std::views namespace, in the <ranges> header.
#include <print>
#include <ranges>
#include <vector>
int main() {
std::vector<int> marks = {45, 78, 92, 33, 60, 88};
auto boosted = marks
| std::views::filter([](int m) { return m >= 40; }) // keep passes
| std::views::transform([](int m) { return m + 5; }); // add grace marks
for (int m : boosted) {
std::print("{} ", m);
}
std::println("");
std::println("Original first mark: {}", marks[0]);
}50 83 97 65 93 Original first mark: 45
Read the pipeline from top to bottom: take marks, keep only the marks of 40 or more, then add 5 to each. The 33 disappears, and the original vector is unchanged, because a view never modifies or copies the data it looks at.
Views are lazy
Building a pipeline does no work. Each element is computed only when the loop asks for it, one at a time. This example proves it by printing inside the transformation:
#include <print>
#include <ranges>
int main() {
auto pipeline = std::views::iota(1, 100)
| std::views::transform([](int n) {
std::println("computing {}", n);
return n * 10;
})
| std::views::take(3);
std::println("pipeline built, nothing computed yet");
for (int value : pipeline) {
std::println("got {}", value);
}
}pipeline built, nothing computed yet computing 1 got 10 computing 2 got 20 computing 3 got 30
std::views::iota(1, 100) produces the numbers 1 to 99 on demand, without storing them. Because take(3) stops after three elements, only three of the 99 numbers are ever transformed. Laziness also means you can work with infinite ranges: std::views::iota(1) counts upwards forever, and that is fine as long as something like take stops the loop.
Useful views
| View | What it produces |
|---|---|
std::views::filter(pred) | Only the elements for which pred returns true |
std::views::transform(f) | f(element) for each element |
std::views::take(n), drop(n) | The first n elements, or all except the first n |
std::views::reverse | The elements in reverse order |
std::views::iota(a, b) | The numbers a, a + 1, ... up to b - 1 |
std::views::keys, values | The first or second part of each pair, as in a map |
#include <map>
#include <print>
#include <ranges>
#include <string>
#include <vector>
int main() {
auto squares = std::views::iota(1)
| std::views::transform([](int n) { return n * n; })
| std::views::take(5);
for (int s : squares) { std::print("{} ", s); }
std::println("");
std::vector<std::string> line = {"Aarav", "Isha", "Kabir", "Meera", "Rohan"};
for (const auto& name : line | std::views::drop(2) | std::views::reverse) {
std::print("{} ", name);
}
std::println("");
std::map<std::string, int> stock = {{"Pens", 120}, {"Rulers", 40}, {"Erasers", 75}};
for (const auto& item : stock | std::views::keys) { std::print("{} ", item); }
int total = 0;
for (int qty : stock | std::views::values) { total += qty; }
std::println("| total items: {}", total);
}1 4 9 16 25 Rohan Meera Kabir Erasers Pens Rulers | total items: 235
drop(2) skips Aarav and Isha, then reverse walks the rest backwards. The map keys come out sorted because std::map keeps them in order.
New in C++23: enumerate, zip and ranges::to
C++23 added several views that remove common hand-written loops, and a way to turn a view back into a real container.
#include <print>
#include <ranges>
#include <string>
#include <tuple>
#include <vector>
int main() {
std::vector<std::string> names = {"Aarav", "Priya", "Kabir"};
std::vector<int> scores = {82, 95, 77};
for (auto [index, name] : std::views::enumerate(names)) {
std::println("{}. {}", index + 1, name);
}
for (auto [name, score] : std::views::zip(names, scores)) {
std::println("{} scored {}", name, score);
}
auto toppers = std::views::zip(names, scores)
| std::views::filter([](const auto& entry) { return std::get<1>(entry) >= 80; })
| std::views::transform([](const auto& entry) { return std::get<0>(entry); })
| std::ranges::to<std::vector>();
std::println("Toppers: {} and {}", toppers[0], toppers[1]);
}1. Aarav 2. Priya 3. Kabir Aarav scored 82 Priya scored 95 Kabir scored 77 Toppers: Aarav and Priya
std::views::enumerategives pairs of (index, element), so you no longer need a separate counter.std::views::zipwalks several ranges side by side, giving one tuple per position and stopping at the shortest range.std::ranges::to<std::vector>()runs the pipeline and stores the results in a new container. Here the element type,std::string, is deduced.
The big rule: a view must not outlive its data
A view only refers to the original elements. If that original container is destroyed, the view dangles, exactly like a std::string_view:
auto passed_marks() {
std::vector<int> marks = {45, 78, 33};
return marks | std::views::filter([](int m) { return m >= 40; }); // dangles!
}marks is destroyed when the function returns, so the returned view points at freed memory. Return a real container instead, for example by ending the pipeline with std::ranges::to<std::vector>(). Also avoid changing the underlying container while you are looping over a view of it.
Common mistakes
- Returning views of local containers. The view dangles; return a container.
- Expecting a view to store results. A view recomputes on every loop; if a transformation is expensive and you loop twice, store the results with
std::ranges::to. - Forgetting
#include <ranges>. Views are not available without it. - Using the pipe on algorithms.
|works with views andstd::ranges::to, not with algorithms likestd::ranges::sort, which you call normally.
Key points
- A view adapts another range without copying; join views with
|into a pipeline. - Views are lazy: elements are computed only when needed, so infinite ranges like
std::views::iota(1)are fine withtake. - Common views:
filter,transform,take,drop,reverse,iota,keysandvalues. - C++23 adds
enumerate,zipandstd::ranges::toto collect results into a container. - A view must never outlive the container it looks at.
Exercise
Given std::vector<int> prices = {499, 1299, 89, 2499, 349, 999};, build a pipeline that keeps prices above 300, applies a 10 percent discount (integer arithmetic: p * 90 / 100), and keeps only the first three results. Print them, then collect the same pipeline into a std::vector<int> with std::ranges::to and print its size.
Show solution
#include <print>
#include <ranges>
#include <vector>
int main() {
std::vector<int> prices = {499, 1299, 89, 2499, 349, 999};
auto deals = prices
| std::views::filter([](int p) { return p > 300; })
| std::views::transform([](int p) { return p * 90 / 100; })
| std::views::take(3);
for (int d : deals) {
std::print("{} ", d);
}
std::println("");
auto saved = deals | std::ranges::to<std::vector>();
std::println("Stored {} deals", saved.size());
}449 1169 2249 Stored 3 deals
The filter removes 89, the discount turns 499 into 449, 1299 into 1169 and 2499 into 2249, and take(3) stops before 349 and 999.
Quick Quiz
1. What does a view do with the data it looks at?
Views are non-owning and lazy; the original container is unchanged.
2. When are the elements of a view pipeline computed?
Views are lazy, which also allows infinite ranges such as std::views::iota(1).
3. Which C++23 facility turns a view into a std::vector?
std::ranges::to runs the pipeline and stores the results in a new container.
4. Why is returning local_vector | std::views::filter(...) from a function a bug?
A view does not own its data, so it dangles once the local container is gone.
Learn C++ with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
