The C++20 standard added Ranges, which are an abstraction of containers that allow the program to operate uniformly on containers' elements. Furthermore, Ranges represent a very modern and concise way of writing expressive code. We'll learn that this expressiveness is even greater with pipes and adaptors.
Learning how Ranges work
How to do it...
In this section, we'll write a program that will help us learn the main use case of Ranges in conjunction with pipes and adaptors. Given an array of temperatures, we want to filter out the negative ones and convert the positives (warm temperatures) into Fahrenheit:
- On a new source file, type the following code. As you can see, two lambda functions and a for range loop does the job:
#include <vector>
#include <iostream>
#include <ranges>
int main()
{
auto temperatures{28, 25, -8, -3, 15, 21, -1};
auto minus = [](int i){ return i <= 0; };
auto toFahrenheit = [](int i) { return (i*(9/5)) + 32; };
for (int t : temperatures | std::views::filter(minus)
| std::views::transform(toFahrenheit))
std::cout << t << ' '; // 82.4 77 59 69.8
}
We'll analyze what's behind of Ranges in the next section. We'll also learn that Ranges are the first users of concepts.
How it works...
std::ranges represents a very modern way of describing a sequence of actions on a container in a readable format. This is one of the cases where the language improves readability.
Step 1 defines the temperatures vector, which contains some data. Then, we defined a lambda function that returns true if the input, i, is greater or equal to zero. The second lambda we defined converts i into Fahrenheit. Then, we looped over temperatures (viewable_range) and piped to the filter (called adaptor, in the scope of Ranges), which removed the negative temperatures based on the minus lambda function. The output is piped to another adaptor that converts every single item of the container so that the final loop can take place and print to the standard output.
C++20 provides another level on top of the one we used to iterate over the container's element, one that's more modern and idiomatic. By combining viewable_range with adaptors, the code is more concise, compact, and readable.
The C++20 standard library provides many more adaptors following the same logic, including std::views::all, std::views::take, and std::views::split.
There's more...
All of the adaptors are templates that use concepts to define the requirements that the specific adaptor needs. An example of this is as follows:
template<ranges::input_range V, std::indirect_unary_predicate<ranges::iterator_t<V>> Pred >
requires ranges::view<V> && std::is_object_v<Pred>
class filter_view : public ranges::view_interface<filter_view<V, Pred>>
This template is the std::views::filter we used in this recipe. This template takes two types: the first one is V, the input range (that is, the container), while the second one is Pred (which is the lambda function, in our case). We've specified two constraints for this template:
- V must be a view
- The predicate must be an object type: a function, lambda, and so on
See also
- The Understanding concepts recipe to review concepts.
- Go to https://github.com/ericniebler/range-v3 to see the range implementation by the C++20 library proposal author (Eric Niebler).
- Learning the Linux fundamentals – shell recipe in Chapter 1, Getting Started with System Programming, to notice that the C++20 Ranges pipe is very similar to the concept of pipes we've seen on the shell.
- To read more about std::is_object, please visit the following link: https://en.cppreference.com/w/cpp/types/is_object.