As an embedded software developer I often have to deal with lots of data from various sensors and there are various degree of processing that has to be done with those data. Now, there are N number of ways we can deal with these data. One of the natural and popular way is to use loops. Let’s create a hypothetical situation where you have a bunch of data in a std::vector<int> and you are supposed to do the following operations on those data (purely hypothetical to make the point):

  • Filter only the values that are positive
  • Calibrate the values using some calibration factor
  • Check if those calibrated values are within certain range
  • Print those final filtered values
  • and so on.

On a usual day you will think of using a loop and process the data to achieve the above. One of the approach could be something like the below:

#include <iostream>
#include <vector>
int main()
{
// do these operation on the input sensor data
// filter the positive values
// calibrate the filtered values
// if the calibrated values are within a range then
// print the processed data
std::vector<int> sensor_readings = { -15, 10, 0, 21, -13, 45, 60, -18, 19 };
std::vector<int> processed;
processed.reserve(sensor_readings.size());
for (auto r : sensor_readings)
{
if (r > 0)
{
int calibrated = r * 2;
if (calibrated >= 20 && calibrated <= 100)
{
processed.push_back(calibrated);
}
}
}
for (auto t : processed)
{
std::cout << t << ' ';
}
std::cout << '\n';
return 0;
}

$ g++ –std=c++20 example_loop_processing.cpp -o example_loop_processing -Wall -Werror
$ ./example_loop_processing
20 42 90 38

This code is simple, you are iterating over the vector and looking for the positive values, is found applying calibration and then checking for the range, if the calibrated value is within the range you are pushing it into another vector. Finally printing the values. Nothing wrong, however the if...else logic, the vector allocation are interesting because as the requirements increase those become more and more crazy to manage. One extra incorrect condition check boom! nothing works anymore.

std::ranges

Here comes the std::ranges with the bunch of filters and other operations that can make our lives way better. Being a linux developer I can think of a grep command and a series of pipe passing the output of one stage to another in a chain and at each stage the output gets a bit filtered or modified and so on. I mean something like this:

cat data.txt | grep error | sort | uniq

The above is self explanatory and may or may not be the best analogy but definitely helps me to conceptualise the idea here. So how does it work? Let’s have a look:

#include <iostream>
#include <ranges>
#include <vector>
int main()
{
// do these operation on the input sensor data
// filter the positive values
// calibrate the filtered values
// if the calibrated values are within a range then
// print the processed data
std::vector<int> sensor_readings = { -15, 10, 0, 21, -13, 45, 60, -18, 19 };
auto processed_view =
sensor_readings | std::views::filter([](int reading) { return reading > 0; })
| std::views::transform([](int reading) { return reading * 2; })
| std::views::filter([](int calibrated) { return calibrated >= 20 && calibrated <= 100; });
for (auto p : processed_view)
{
std::cout << p << ' ';
}
std::cout << '\n';
return 0;
}

$ g++ –std=c++20 example_range_processing.cpp -o example_range_processing
$ ./example_range_processing
20 42 90 38

Ok, what have we just done? We have applied a filter on the input vector sensor_readings<int>. The std::views provides such useful utilities like std::views::filter which is doing the filtering of the positive values from the input vector, then the std::views::transform is applying our calibration factor on the filtered values, and then finally std::views::filter again checking the calibrated value range and accordingly accepting or rejecting the value for the final view. That’s it. We didn’t have to write those convoluted if...else logic (it gets messy believe me!), didn’t have to create and resize additional std::vector<int> to hold the processed values, much cleaner and better in many ways.

However, remember that std::views are not real containers so it doesn’t hold anything for you, if your underlying container gone then the operations related to view may be invalid or may get you into undefined behaviour. So std::views are totally dependent on the underlying containers (source or range). So depending on your requirement you may have to decide if this is the best option for you or not.

Also, if the call path is repeatedly visited then the whole operation of filtering and transforming etc will be repeated again and again increasing the CPU utilisation and hence won’t be good for performance. In a situation like that you may be better off storing the output in a dedicated container. But again this is a feature which has to be understood well and used where it is applicable not indiscriminately everywhere.

Good thing is you can chain as many view adaptors, such as filters and transforms, as you need. For example the above code can be slightly modified to add another calibration of sort as below:

auto processed_view =
sensor_readings | std::views::filter([](int reading) { return reading > 0; })
| std::views::transform([](int reading) { return reading * 2; })
| std::views::filter([](int calibrated) { return calibrated >= 20 && calibrated <= 100; })
| std::views::transform([](int calibrated){return calibrated / 2;});

I have just added another transform operation in the list and the output is:

$ g++ –std=c++20 example_range_processing.cpp -o example_range_processing
$ ./example_range_processing
10 21 45 19

So you can pick and choose what you want in that pipeline and get your results accordingly.

So if you have not tried it yet, try it yourself and see if you like. Perhaps goes without saying, some of these benefits may or may not be achievable depending on what we are trying to do. Nonetheless these features are there to enhance your code. Hence take a look, understand how it works and decide for yourself.

The source code can be found in the below repository:

C++ 20 std::range Sample Code

Leave a Reply