Ranges (C++20)
Ranges are a new element of the STL (in C++20, with extensions in C++23) that makes using algorithms more concise and adds the ability to write code in a functional style.
Range algorithms:
- are lazy (they compute a value just before it is needed),
- operate directly on a container, not on its iterators, and
- can be composed.
Furthermore, most of the classical STL algorithms have range versions, which support projections and provide additional safety guarantees.
[...]
Motivation/Advantages
Avoid code duplication*
Consistency*
Safety
Concepts ensure that algorithms can operate on ranges in a type-safe manner.
std::list<int> numbers{5, 6, 1, 3, 7}; // std::list is bidirectional_range std::ranges::sort(numbers); // Compilation error: random_access_range not satisfied
...
Lazy Evaluation
A view is just the specification of a processing. The processing does not happen when one defines the view but is actually performed element by element when the next value is asked for.
An example:
int main() { auto lazySquares = std::views::iota(1, 10) | std::views::transform([](int i){ return i * i; }); for (auto n : lazySquares) { std::cout << n << ' '; // Squares are calculated here }
Composability*
A Very Simple Example
// rangesFilterTransform.cpp #include<iostream> #include<ranges> // ... std::vector<int>numbers = {1, 2, 3, 4, 5, 6}; autoresults = numbers | std::views::filter([] (int n) {return n%2 == 0;}) | std::views::transform([] (int n) {return n*2;}); for(auto v : results) std::cout<<v<<" ";// 4 8 12
You have to read the expression from left to right. The pipe symbol (|) stands for function composition: First, all numbers can pass which are even (std::views::filter([](int n){ return n % 2 == 0; }))
. Afterwards, each remaining number is mapped to its double (std::views::transform([](int n){ return n * 2; })
).
Terminology
Range
A range is a type with an iterator pair consisting of a begin()
and end()
function:
struct Range { T begin(); T end(); };
The code above models a classical STL container. For at least some ranges, we need something more general that does not only model a finite range. We usually compare begin and end for equality and stop the iterations as soon as that condition is true. For an infinite range, which is possible with ranges, end is usually a different type than that for begin. The STL brings us a new type for convenience: std::default_sentinel_t
:
struct Range { T begin(); std::default_sentinel_t end(); };
You can see std::default_sentinel_t
as a tag type, which simply enables end()
to have a return type and match the correct overload of operator==
later on.
Sentinel
The sentinel specifies the end of a range. For the containers of the STL, the end iterator is the sentinel. Starting C++20, the type of the sentinel can be different from the type of the begin iterator.
The following example uses a space as a sentinel.
#include<algorithm> #include<iostream> struct Space{ bool operator==(auto pos) const { return *pos == ' '; } // ... const char *rainerGrimm = "Rainer Grimm"; std::ranges::for_each(rainerGrimm, Space{}, [](char c){std::cout<<c;});
Output:
Rainer
Since the space works as a sentinel, only Rainer
is displayed in the last line.
common_range
Ranges, as explained, consist of an end()
member function that provides a sentinel. A common_range
, on the other hand, is a range where begin(r)
and end(r)
return the same type. This implies that a common_range
's end()
does not return a sentinel.
All classic iterators model a common_range
. Moreover, most STL types have a common_range constructor.
sized_range
*
Views
Views are lightweight ranges. A view allows you to access ranges, iterate through ranges, or modify or filter elements of a range. A view does not own data, and its time complexity to copy, move, or assign is constant.
Range Adaptors
A range adaptor transforms a range into a view.
Example:
std::vector<int> numbers = {1, 2, 3, 4, 5, 6}; auto results = numbers | std::views::filter([] (int n) {return n%2==0;}) | std::views::transform([](int n){return n*2;});
In this code snippet, numbers is the range, and std::views::filter
and std::views::transform
are the views.
The ranges library in C++20 has a rich set of views...
Owning and non-owning C++ Ranges
(From https://hannes.hauswedell.net/post/2025/05/17/non-owning-range/, by Hannes Hauswedell)
Containers are the ranges everybody used before ranges were a thing. They own their elements, i.e. the storage of the elements is managed by the container and the elements disappear when the container does.
Containers are multi-pass ranges, i.e. you can iterate over them multiple times and will always observe the same elements.
If containers are owning ranges, what are non-owning ranges? C++17 introduced a first example: std::string_view
, a range that consists just of a begin and end pointer into another range's elements.
In general, non-owning ranges are the subset of ranges that do not manage the memory of their elements. std::string_view
is such a range, and it even fulfils the stricter requirements of a borrowed range: A range that stores nothing beside the iterators, and where the iterators may not refer back to the range object. This has the important implication that the iterators of a borrowed range remain valid when the borrowed range itself goes out-of-scope. For a string_view
, this design seems natural, but we will later encounter ranges where a trade-off has to be made.
ranges::to
std::ranges::to
is a convenient way in C++23 to construct a container from a range:
std::vector<int> range(int begin, int end, int stepsize=1) { auto boundary = [end](int i) {return i<end;}; std::vector<int> result = std::ranges::views::iota(begin) | std::views::stride(stepsize) |std::views::take_while(boundary) |std::ranges::to<std::vector>(); return result; }
The preceding function range
creates a std::vector<int> consisting of all elements for begin to end with the stepsize stepsize. Understandably, begin must be smaller than end.
Views
Function Composition, Pipelines, Chaining
Because a view is a range, one can use a view as an argument for another view to enable chaining:
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; std::views::reverse( std::views::take( std::views::transform( std::views::filter(numbers, [](const auto& n) { return n % 2 == 0; }), [](const auto& n) { return n * n; }), // transformation 4) // how many items to "take" or allow through )
Excessive nesting can lead to readability and maintenance issues.
There is another way to combine ranges and views by using operator |
(pipe):
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; auto result = numbers | std::views::filter([](const auto& n) { return n % 2 == 0; }) | std::views::transform([](const auto& n) { return n * n; }) | std::views::take(4) | std::views::reverse;
Bounded vs. Unbounded (Infinite) Range
We shall be using std::views::iota
, a range factory that generates a sequence of elements by repeatedly incrementing an initial value.
An iota
invokation can be either bounded or unbounded (infinite). For instance, std::views::iota(10)
is lazy. It only produces a new value if it is asked for, starting at number 10.
std::vector<int> vec1, vec2, vec3; // Use bounded std::views::iota with begin and end value: for (int i : std::views::iota(0, 10)) vec1.push_back(i); // Use infinite std::views::iota in combination with std::views::take: for (int i : std::views::iota(0) | std::views::take(10)) vec2.push_back(i); // Use infinite std::views::iota in combination with std::views::take_while: for (int i : std::views::iota(0) | std::views::take_while([](int y) { return y < 10; })) vec3.push_back(i);
All 3 vectors are equal. They are: 0 1 2 3 4 5 6 7 8 9
Projections
Many range-based algorithms have a projection parameter, a callback that transforms each element before it is processed.
One example of projection parameter is found in the declaration of the std::ranges::sort()
function.
template<ranges::random_access_range R, class Comp = ranges::less, class Proj = std::identity> requires std::sortable<ranges::iterator_t<R>, Comp, Proj> constexpr ranges::borrowed_iterator_t<R> sort( R&& r, Comp comp = {}, Proj proj = {} );
For instance, you could use a projection to sort numbers by absolute value. Just make the last, third parameter a lambda transforming to absolute:
std::vector<int> numbers = {-8, 4, -3, 2, -5, 10, 7, 1, -9}; std::ranges::sort(numbers, std::ranges::less{}, [] (auto n) { return std::abs(n); });
Yet another possibility is to sort (say, a structure o class) by a specific field. For instance, often enough you would rather sort Person
objects
struct Person{ std::string name{}; std::size age{}; };
by name:
std::vector<Person> persons{ {"A", 10}, {"B", 2},{"C", 30}, {"D", 67}, {"E", 23}, {"F", 42} }; std::ranges::sort(persons, {}, &Person::name);
We could alternatively settle of descending order: std::ranges::greater()
:
std::ranges::sort(persons, std::ranges::greater(), &Person::name);
Keys View and Values View
You might want to get the keys and values of an associative container such as a std::map
, or an std::unordered_map
etc.
In the examples below, note different ways to write the same code.
std::map<std::string, int> numberMap{ {"one", 1}, {"two", 2}, {"three", 3}, {"four", 4}, {"five", 5}}; auto strings = std::views::keys(numberMap); for (const auto& s : strings){ std::cout << s << " "; } for (const auto& s : std::views::keys(numberMap)){ std::cout << s << " "; } for (const auto& s : numberMap | std::views::keys){ std::cout << s << " "; } auto numbers = std::views::values(numberMap); for (const auto& n : numbers){ std::cout << n << " "; } for (const auto& n : std::views::values(numberMap)) { std::cout << n << " ";} for (const auto& n : numberMap | std::views::values) { std::cout << n << " ";}
Predefined Views in C++20
Here's the list of predefined views that we get with C++20:
Name | Notes |
---|---|
views::all | returns a view that includes all elements of its range argument. |
filter_view/filter | returns a view of the elements of an underlying sequence that satisfy a predicate. |
transform_view/transform | returns a view of an underlying sequence after applying a transformation function to each element. |
take_view/take | returns a view of the first N elements from another view, or all the elements if the adapted view contains fewer than N. |
take_while_view/take_while | Given a unary predicate pred and a view r, it produces a view of the range [begin(r), ranges::find_if_not(r, pred)). |
drop_view/drop | returns a view excluding the first N elements from another view, or an empty range if the adapted view contains fewer than N elements. |
drop_while_view/drop_while | Given a unary predicate pred and a view r, it produces a view of the range [ranges::find_if_not(r, pred), ranges::end(r)). |
join_view/join | It flattens a view of ranges into a view |
split_view/split | It takes a view and a delimiter and splits the view into subranges on the delimiter. The delimiter can be a single element or a view of elements. |
counted | A counted view presents a view of the elements of the counted range ([iterator.requirements.general]) i+[0, n) for an iterator i and non-negative integer n. |
common_view/common | takes a view which has different types for its iterator and sentinel and turns it into a view of the same elements with an iterator and sentinel of the same type. It is useful for calling legacy algorithms that expect a range’s iterator and sentinel types to be the same. |
reverse_view/reverse | It takes a bidirectional view and produces another view that iterates the same elements in reverse order. |
elements_view/elements | It takes a view of tuple-like values and a size_t, and produces a view with a value-type of the Nth element of the adapted view’s value-type. |
keys_view/keys | Takes a view of tuple-like values (e.g. std::tuple or std::pair), and produces a view with a value-type of the first element of the adapted view’s value-type. It’s an alias for elements_view<views::all_t<R>, 0>. |
values_view/values | Takes a view of tuple-like values (e.g. std::tuple or std::pair), and produces a view with a value-type of the second element of the adapted view’s value-type. It’s an alias for elements_view<views::all_t<R>, 1>. |
Predefined Views in C++20
Here's the list of additional predefined views that we get with C++23:
Name | Notes |
---|---|
repeat_view/views::repeat | a view consisting of a generated sequence by repeatedly producing the same value |
cartesian_product_view/views::cartesian_product | a view consisting of tuples of results calculated by the n-ary cartesian product of the adapted views |
zip_view/views::zip | a view consisting of tuples of references to corresponding elements of the adapted views |
zip_transform_view/views::zip_transform | a view consisting of tuples of results of application of a transformation function to corresponding elements of the adapted views |
adjacent_view/views::adjacent | a view consisting of tuples of references to adjacent elements of the adapted view |
adjacent_transform_view/views::adjacent_transform | a view consisting of tuples of results of application of a transformation function to adjacent elements of the adapted view |
join_with_view/views::join_with | a view consisting of the sequence obtained from flattening a view of ranges, with the delimiter in between elements |
slide_view/views::slide | a view whose Mth element is a view over the Mth through (M + N - 1)th elements of another view |
ranges::chunk_view/views::chunk | a range of views that are N-sized non-overlapping successive chunks of the elements of another view |
ranges::chunk_by_view/views::chunk_by | splits the view into subranges between each pair of adjacent elements for which the given predicate returns false |
ranges::as_const_view/views::as_const | converts a view into a constant_range |
ranges::as_rvalue_view/views::as_rvalue | a view of a sequence that casts each element to an rvalue |
ranges::stride_view/views::stride | a view consisting of elements of another view, advancing over N elements at a time |
Some Noteworthy, Illustrative, perhaps Useful Examples
Examplle with std::variant
We are going to:
- Create a vector containing numbers and strings (mixedNumbers) as variants
- Filter out the strings using
std::views::filter
- Print only the items actually holding a string instead of a number (
holds_alternative<std::string>
)
int main() { std::vector<std::variant<int, std::string>> mixedNumbers = {1, 2, 3, "four", "five"}; auto stringValues = mixedData | std::views::filter([](const auto& val) { return std::holds_alternative<std::string>(val); }); for (const auto& str : stringValues) { std::cout << std::get<std::string>(str) << " "; }// Output: four five return 0; }
Mapping/Transforming Elements (into a Different Type, Actually)
Transforming elements of a range (mapping in other contexts) doesn't require the resulting range to have elements of the same type. One can map elements to another type. For instance, from integers to strings.
int main() { std::vector<int> numbers { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; std::map<int, std::string> numberMap{ {1, "one"}, {2, "two"}, {3, "three"}, {4, "four"}, {5, "five"} }; auto result = numbers | std::views::filter([] (const auto& n) { return n <= 5; }) | std::views::transform([&numberMap] (const auto& n) { return numberMap[n]; }); for (const auto& str : result) { std::cout << str << " "; }// Output: one two three four five }
Filtering and Accumulating Elements (Using std::views::common
)
We will be:
- Combining
std::ranges::views
andstd::accumulate
- Since
std::accumulate
is not range-based algorithm, we will be relying onstd::views::common
, which creates a view with same-typed begin and end iterators.
Note std::views::common
is similar to std::views::all
, but it generates a std::ranges::common_view
if the range has iterators with different types.
// ... #include <numeric> #include <ranges> int main() { std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9}; auto numbers = std::views::iota(0) | std::views::take(10); auto evenNumbers = numbers | std::views::filter([](int n){ return n % 2 == 0; }) | std::views::common; int sum = std::accumulate(evenNumbers.begin(), evenNumbers.end(), 0); std::cout << sum;// Output: 20 }
Views all_of
, any_of
, none_of
In the example below, given a vector of integers (numbers), find (1) if any element is negative, (2) if none of the elements are negative, (3) if all elements are even
// ... int main() { std::vector<int> numbers = {2, 4, 6, 8, 10}; bool anyNegative = std::ranges::any_of(numbers, [](int x) { return x < 0; }); // false bool noneNegative = std::ranges::none_of(numbers, [](int x) { return x < 0; }); // true bool allEven = std::ranges::all_of(numbers, [](int x) { return x % 2 == 0; }); //true return 0; }