Ranges and views

The algorithms of lesson 050 take two iterators; the range versions take the container. std::ranges::sort(values) says what it does without the begin and end, and a view is a lazy transformation that computes an element only when it is read.

Views compose with the pipe operator: filter the odd numbers, double them, take the first three. Nothing is copied and nothing is computed until the loop asks for the next element, which is why a view over a large range costs nothing to create.

A view is cheap to create and cheap to copy, but it must not outlive the range it looks at; when a pipeline is stored in a variable that outlives the container, ranges::to is the safe way to keep the data.

The views live in <ranges> and the algorithms in <algorithm>; a pipeline that only reads is a view, while one that writes back is an algorithm such as std::ranges::for_each or std::ranges::sort.

The range algorithms

std::ranges::sort takes a range instead of two iterators. The program sorts a vector and checks the result with std::ranges::is_sorted, which also takes the container.

The range versions are the same algorithms with the iterator pair hidden inside a concept. The error messages are shorter as well, because a container that does not satisfy the range concept fails one check instead of a template trace.

They live in <algorithm> and <ranges> and were added in C++20. The iterator versions remain valid, and a library that supports C++17 still uses them.

#include <algorithm> #include <iostream> #include <ranges> #include <vector> int main() { std::vector<int> values{5, 1, 4, 2}; std::ranges::sort(values); std::cout << std::ranges::is_sorted(values) << ' '; for (int value : values) { std::cout << value << ' '; } std::cout << '\n'; }

g++ -std=c++23 -Wall -Wextra sort.cpp -o ranges_sort
./ranges_sort

1 1 2 4 5

A filter view

views::filter produces a view of the elements that satisfy a predicate. The program filters the even numbers out of a vector and prints what is left; no vector is created for the result.

The predicate is called for each element as the loop advances, so a view over a million elements costs nothing until the loop walks it. The view refers to the original container and must not outlive it.

A filter view is not a container: it has no size() unless the underlying range does, and it cannot be indexed. It is a range, and a range-for is the natural way to read it.

#include <iostream> #include <ranges> #include <vector> int main() { std::vector<int> values{1, 2, 3, 4, 5, 6}; auto even = values | std::views::filter([](int value) { return value % 2 == 0; }); for (int value : even) { std::cout << value << ' '; } std::cout << '\n'; }

g++ -std=c++23 -Wall -Wextra filter.cpp -o ranges_filter
./ranges_filter

2 4 6

Filter, transform, take

Views compose with the pipe operator. The program takes the odd numbers, doubles them and keeps the first three, and the whole pipeline reads top to bottom.

The evaluation is lazy and element by element: the loop asks for one result, the transform asks the filter for one element, and the filter advances the source until it finds an odd number. Elements after the third are never touched.

The order of the stages matters for the work done. A filter before a transform runs the transform fewer times, which is why the example puts it first.

#include <iostream> #include <ranges> #include <vector> int main() { std::vector<int> values{1, 2, 3, 4, 5, 6, 7, 8}; auto result = values | std::views::filter([](int value) { return value % 2 == 1; }) | std::views::transform([](int value) { return value * 2; }) | std::views::take(3); for (int value : result) { std::cout << value << ' '; } std::cout << '\n'; }

g++ -std=c++23 -Wall -Wextra pipeline.cpp -o pipeline
./pipeline

2 6 10

Materialising a view

A view is not a container. When the result has to be stored, std::ranges::to<std::vector<int>>() builds one from the range, which is the C++23 way to collect a pipeline.

The program filters, transforms and collects the result into a vector, then prints its size and its elements. The vector owns its data and can outlive the source.

Before C++23 the same job was a loop with push_back or std::copy into a back_inserter. The range version is shorter and does not change the algorithm.

#include <iostream> #include <ranges> #include <vector> int main() { std::vector<int> values{1, 2, 3, 4, 5, 6}; auto squared = values | std::views::filter([](int value) { return value % 2 == 0; }) | std::views::transform([](int value) { return value * value; }) | std::ranges::to<std::vector<int>>(); std::cout << squared.size() << ':'; for (int value : squared) { std::cout << ' ' << value; } std::cout << '\n'; }

g++ -std=c++23 -Wall -Wextra to.cpp -o ranges_to
./ranges_to

3: 4 16 36

A const range cannot be sorted

std::ranges::sort needs a mutable range, and a const container is not one. The compiler reports that no overload matches and lists the sortable constraint that failed.

The program passes a const vector to the range sort. The note names the constraint, which is the useful part: the range version says what the algorithm needs instead of failing inside a template instantiation.

The fix is to remove const, or to sort a copy. A function that takes its range by const reference cannot sort it, and that is the correct behaviour.

#include <algorithm> #include <vector> int main() { const std::vector<int> values{3, 1, 2}; std::ranges::sort(values); return values[0]; }

g++ -std=c++23 -Wall -Wextra const_sort.cpp -o const_sort

const_sort.cpp: In function 'int main()':
const_sort.cpp:7:22: error: no match for call to '(const std::ranges::__sort_fn) (const std::vector<int>&)'
7 | std::ranges::sort(values);
| ~~~~~~~~~~~~~~~~~^~~~~~~~
In file included from /usr/include/c++/15.2.0/algorithm:65,
from const_sort.cpp:1:
/usr/include/c++/15.2.0/bits/ranges_algo.h:1812:10: note: there are 2 candidates
1812 | struct __sort_fn
| ^~~~~~~~~
/usr/include/c++/15.2.0/bits/ranges_algo.h:1818:7: note: candidate 1: 'template<class _Iter, class _Sent, class _Comp, class _Proj> requires (random_access_iterator<_Iter>) && (sentinel_for<_Sent, _Iter>) && (sortable<_Iter, _Comp, _Proj>) constexpr _Iter std::ranges::__sort_fn::operator()(_Iter, _Sent, _Comp, _Proj) const'
1818 | operator()(_Iter __first, _Sent __last,
| ^~~~~~~~
/usr/include/c++/15.2.0/bits/ranges_algo.h:1818:7: note: candidate expects 2 arguments, 1 provided
/usr/include/c++/15.2.0/bits/ranges_algo.h:1831:7: note: candidate 2: 'template<class _Range, class _Comp, class _Proj> requires (random_access_range<_Range>) && (sortable<decltype(std::ranges::__access::__begin((declval<_Container&>)())), _Comp, _Proj>) constexpr std::ranges::borrowed_iterator_t<_Range> std::ranges::__sort_fn::operator()(_Range&&, _Comp, _Proj) const'
1831 | operator()(_Range&& __r, _Comp __comp = {}, _Proj __proj = {}) const
| ^~~~~~~~
/usr/include/c++/15.2.0/bits/ranges_algo.h:1831:7: note: template argument deduction/substitution failed:
/usr/include/c++/15.2.0/bits/ranges_algo.h:1831:7: note: constraints not satisfied
In file included from /usr/include/c++/15.2.0/bits/stl_iterator_base_types.h:73,
from /usr/include/c++/15.2.0/bits/stl_algobase.h:65,
from /usr/include/c++/15.2.0/algorithm:62:
/usr/include/c++/15.2.0/bits/iterator_concepts.h: In substitution of 'template<class _Range, class _Comp, class _Proj> requires (random_access_range<_Range>) && (sortable<decltype(std::ranges::__access::__begin((declval<_Container&>)())), _Comp, _Proj>) constexpr std::ranges::borrowed_iterator_t<_Range> std::ranges::__sort_fn::operator()(_Range&&, _Comp, _Proj) const [with _Range = const std::vector<int>&; _Comp = std::ranges::less; _Proj = std::identity]':
const_sort.cpp:7:22: required from here
7 | std::ranges::sort(values);
| ~~~~~~~~~~~~~~~~~^~~~~~~~
/usr/include/c++/15.2.0/bits/iterator_concepts.h:599:13: required for the satisfaction of 'indirectly_writable<_Out, std::iter_rvalue_reference_t<_In> >' [with _Out = __gnu_cxx::__normal_iterator<const int*, std::vector<int, std::allocator<int> > >; _In = __gnu_cxx::__normal_iterator<const int*, std::vector<int, std::allocator<int> > >]
/usr/include/c++/15.2.0/bits/iterator_concepts.h:862:13: required for the satisfaction of 'indirectly_movable<_In, _Out>' [with _In = __gnu_cxx::__normal_iterator<const int*, std::vector<int, std::allocator<int> > >; _Out = __gnu_cxx::__normal_iterator<const int*, std::vector<int, std::allocator<int> > >]
/usr/include/c++/15.2.0/bits/iterator_concepts.h:866:13: required for the satisfaction of 'indirectly_movable_storable<_Iter, _Iter>' [with _Iter = __gnu_cxx::__normal_iterator<const int*, std::vector<int, std::allocator<int> > >]
/usr/include/c++/15.2.0/bits/iterator_concepts.h:990:13: required for the satisfaction of 'permutable<_Iter>' [with _Iter = __gnu_cxx::__normal_iterator<const int*, std::vector<int, std::allocator<int> > >]
/usr/include/c++/15.2.0/bits/iterator_concepts.h:1007:13: required for the satisfaction of 'sortable<decltype (std::ranges::__access::__begin(declval<_Container&>())), _Comp, _Proj>' [with _Range = const std::vector<int, std::allocator<int> >&; _Comp = std::ranges::less; _Proj = std::identity]
/usr/include/c++/15.2.0/bits/iterator_concepts.h:599:35: in requirements with '_Out&& __o', '_Tp&& __t' [with _Out = __gnu_cxx::__normal_iterator<const int*, std::vector<int, std::allocator<int> > >; _Tp = const int&&]
/usr/include/c++/15.2.0/bits/iterator_concepts.h:601:14: note: the required expression '*__o =(forward<_Tp>)(__t)' is invalid
601 | *__o = std::forward<_Tp>(__t);
| ~~~~~^~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/iterator_concepts.h:602:34: note: the required expression '*(forward<_Out>)(__o)=(forward<_Tp>)(__t)' is invalid
602 | *std::forward<_Out>(__o) = std::forward<_Tp>(__t);
| ~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/iterator_concepts.h:604:11: note: the required expression 'const_cast<const decltype(*(declval<_Iter&>)())&&>(*__o) =(forward<_Tp>)(__t)' is invalid
603 | const_cast<const iter_reference_t<_Out>&&>(*__o)
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
604 | = std::forward<_Tp>(__t);
| ^~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/iterator_concepts.h:606:11: note: the required expression 'const_cast<const decltype(*(declval<_Iter&>)())&&>(*(forward<_Out>)(__o))=(forward<_Tp>)(__t)' is invalid
605 | const_cast<const iter_reference_t<_Out>&&>(*std::forward<_Out>(__o))
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
606 | = std::forward<_Tp>(__t);
| ^~~~~~~~~~~~~~~~~~~~~~~~
cc1plus: note: set '-fconcepts-diagnostics-depth=' to at least 2 for more detail

Exercises

  1. Print the first five even numbers of a range with views::filter and views::take.
  2. Build a pipeline that takes the length of every word in a vector of strings and collect it into a vector.
  3. Reverse a vector with views::reverse and print it, then compare with std::ranges::reverse.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does a range algorithm take instead of two iterators?

    Answer

    The range itself

    The range carries begin and end, so the call names the container once.

  2. What is a view?

    Answer

    A lazy transformation of a range

    The view computes elements on demand and stores no elements of its own.

  3. What does std::views::filter do?

    Answer

    It skips the elements that fail the predicate

    The view yields only the elements for which the predicate is true.

  4. What does std::ranges::to<std::vector>() do?

    Answer

    It materialises a view into a container

    The adapter collects the lazy elements into a real container.

  5. What does this program print? #include <algorithm> #include <iostream> #include <vector> int main() { std::vector<int> values{5, 1, 4, 1, 2}; std::ranges::sort(values); for (int v : values) std::cout << v << ' '; }

    Answer

    1 1 2 4 5

    The range sort orders the whole container, and the loop prints the five values.

  6. What does this program print? #include <iostream> #include <ranges> #include <vector> int main() { std::vector<int> values{1, 2, 3, 4, 5, 6}; for (int v : values | std::views::filter([](int x) { return x % 2 == 0; })) std::cout << v << ' '; }

    Answer

    2 4 6

    The filter view keeps the even elements and the loop reads them lazily.

  7. What does this program print? #include <iostream> #include <ranges> #include <vector> int main() { std::vector<int> values{1, 2, 3, 4, 5}; auto view = values | std::views::filter([](int x) { return x % 2 == 1; }) | std::views::transform([](int x) { return x * 2; }); for (int v : view) std::cout << v << ' '; }

    Answer

    2 6 10

    The odd values are doubled by the pipeline, and the loop prints the results in order.

  8. What does this program print? #include <iostream> #include <ranges> #include <vector> int main() { std::vector<int> values{1, 2, 3, 4}; auto squares = values | std::views::transform([](int x) { return x * x; }); std::vector<int> result; for (int v : squares) result.push_back(v); std::cout << result.size() << ": " << result[1] << ' ' << result[3]; }

    Answer

    4: 4 16

    The view squares each element, and the collected vector holds 1, 4, 9 and 16.

  9. What does the compiler report for this call? #include <algorithm> #include <vector> int main() { const std::vector<int> values{3, 1, 2}; std::ranges::sort(values); }

    Answer

    no match for call to '(const std::ranges::__sort_fn) (const std::vector<int>&)'

    A const range cannot be sorted, and the message names the const range type.

  10. When does a view compute its elements?

    Answer

    When they are read

    The laziness is what makes a view cheap and what makes its lifetime tied to the source.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev

Banner image