Home/Blog/Learn to Code/Learn about ranges in C++20
Home/Blog/Learn to Code/Learn about ranges in C++20

Learn about ranges in C++20

6 min read
Apr 22, 2024
content
The ranges library
Defining ranges
Finding the maximum element
Filtering even numbers
Composing multiple operators
Generating infinite sequence
Conclusion

The most recent version of C++ was released in 2020 as C++20. It includes several enhancements and new features that increase the flexibility and capability of the language. This blog examines some of the most essential and valuable features of C++20 with code examples that might be helpful for programmers.

Let's first discuss the ranges library with various examples.

The ranges library#

In previous versions of C++, manipulating collections of data often required writing custom loops and algorithms. The std::ranges namespace is a new addition to the STL in C++20 that provides a new set of algorithms and views for working with sequences of elements, such as arrays, containers, and streams. The ranges library is designed to be more efficient than traditional iterators and can be used with both const and non-const ranges. It provides a more expressive, intuitive, and composable way to manipulate collections of data, making it easier to write efficient, readable, and better-performing code.

For clarity, let's remember the following:

  • A range is a collection of objects that can be iterated. Many data structures provide methods like begin() and end() to facilitate traversing.

  • A view, on the other hand, is a range that transforms a range and is a lazy-evaluated object. A view returns data from the underlying range but does not own any data.

  • Lastly, a view adaptor is an object that takes a range and returns a view.

Defining ranges#

The ranges library can be easily defined as follows:

#include <ranges>
using namespace std::ranges;

We can use ranges by including the <ranges> library (line 1) and using the std::ranges namespace. The view and view adaptors are in the std::ranges::views namespace. The <ranges> library supports functional patterns, and therefore composition can be performed using the pipe symbol.

The std::ranges namespace includes a wide range of new algorithms, sometimes categorized as modifying, nonmodifying, partitioning, sorting, and permutation operations. We'll discuss a few examples of using ranges that contrast with the traditional code.

Finding the maximum element#

Here’s an example of using ranges to find the largest element in a vector in C++20.

#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::vector<int> v{100, 902, 230, 640, 540};
//Determine max element from the vector using <ranges>
auto max_element = std::ranges::max_element(v);
std::cout << *max_element << std::endl; // Output: 902
return 0;
}

In this example, we create a vector of integers v and use the std::ranges::max_element method to find the largest element. The algorithm returns an iterator to the largest element, which we then dereference in line 10 to get the actual value.

Let's compare the code above with the following traditional code:

#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v{100, 902, 139, 645, 549};
//Determine max element from the vector
auto max_element = std::max_element(v.begin(), v.end());
std::cout << *max_element << std::endl; // Output: 902
return 0;
}

In the code above, we use the std::max_element method with parameters indicating the start and end of the vector to find the maximum element.

The code written in C++20 is considered better in terms of modernity, readability, maintenance, and conciseness. The use of the std::ranges::max_element method simplifies the code by directly operating on the vector v without specifying the boundaries, making it more intuitive and less error-prone.

Filtering even numbers#

Let's take the example of printing even numbers stored in a vector.

In C++20, the task can be achieved by using the std::ranges::views namespace.

  1. In the following code, we define a vector of integers and call it v.

  2. We use a Lambda function [](int x) as a predicate in the filter() method to determine the even numbers (lines 9–11). The Lambda function returns true when the argument is even.

  3. The resulting view even_numbers represents a sequence of even numbers from the vector v.

  4. We then iterate over the view and print each element.

#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> v {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
//Filter takes a predicate function for evaluation
auto even_numbers = v | std::ranges::views::filter([](int x) {
return x % 2 == 0;
});
for (int n : even_numbers) {
std::cout << n << " ";
}
std::cout << std::endl; // Output: 2 4 6 8 10
return 0;
}

The filter() method is a lazy data structure that enables viewing the filtered elements only when requested. This lazy evaluation allows for combining or composing views without incurring a performance penalty.

Let's write the same code in a traditional style. It would look something like this:

#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (const auto& num : numbers) {
if (num % 2 == 0) {
std::cout << num << " ";
}
}
std::cout << std::endl; // Output: 2 4 6 8 10
return 0;
}

In the code above, a for loop (line 6–10) is used to iterate over the vector numbers and evaluate each number. When the number is even, it prints the number. It can be easily seen that the code written using ranges is more usable, maintainable, and modular.

Composing multiple operators#

A cool feature of ranges is that different views can easily be composed to perform a complex task. Let's take a look at the following example:

#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
// Composing using "|" operator
auto result = numbers | std::ranges::views::filter([](int x) { return x % 2 == 0; }) // Filter even numbers
| std::ranges::views::transform([](int x) { return x * 2; }) // Double each number
| std::ranges::views::reverse // Reverse the order
| std::ranges::views::take(5) // Take the first 5 elements
| std::ranges::views::transform([](int x) { return x + 1; }); // Increment each element by 1
for (int num : result) {
std::cout << num << " ";
}
std::cout << std::endl; //29 25 21 17 13
return 0;
}

Here in lines 10–14, we create a view named result by first applying the filter() method that selects only even numbers and then the transform() method that multiplies the selected numbers by 2. Next, we reverse the view using reverse. Then, we select the first 5 elements of the view using the take() method and add 1 to each. Once the view is completed, the for loop iterates over the view and prints the values. Please note that the sequence of operators does matter.

Now, let's consider the same code written in the traditional way.

#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
std::vector<int> even_numbers;
// Filter even numbers
std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(even_numbers), [](int x) { return x % 2 == 0; });
// Double each number
std::transform(even_numbers.begin(), even_numbers.end(), even_numbers.begin(), [](int x) { return x * 2; });
// Reverse the order
std::reverse(even_numbers.begin(), even_numbers.end());
// Take the first 5 elements
if (even_numbers.size() > 5) {
even_numbers.resize(5);
}
// Increment each element by 1
for (int& num : even_numbers) {
num += 1;
}
for (int num : even_numbers) {
std::cout << num << " ";
}
std::cout << std::endl; //29 25 21 17 13
return 0;
}

It can be clearly seen that in order to perform the same operations, we declare a vector even_numbers that takes up memory (line 7). Next, we apply different operations separately, so the even_numbers vector is updated at each step. However, in C++20, thanks to the lazy evaluation of ranges, no memory was allocated to the result and the view was generated when it was needed.

Generating infinite sequence#

We can generate infinite sequences in C++20 thanks to the lazy evaluation of ranges.

In the following example, an infinite Fibonacci series is generated.

#include <iostream>
#include <ranges>
int main() {
// First create a view for infinite range and then transform the range in Fibonacci numbers
auto fibonacci = std::ranges::views::iota(0) | std::ranges::views::transform([a = 0, b = 1]
(int) mutable {
auto result = a;
a = b;
b = result + b;
return result;
}
);
// Print the first 20 Fibonacci numbers
for (int num : fibonacci | std::ranges::views::take(20)) {
std::cout << num << " ";
}
//Output: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181
return 0;
}

In the code above, we create a view (lines 6–13) that will contain the data of the infinite sequence. The std::ranges::views::take method specifies how many numbers will be taken from the view and will be printed.

Writing code for a sequence generator in the traditional way would be a bit complex. In that case, we have to write our own class with an iterator and referencing operator to make the code work. Let's take a look at the following code for better understanding.

#include <iostream>
class InfiniteFibonacciSequence {
public:
//Initialize variables
InfiniteFibonacciSequence() : n1(0), n2(1), fib(0) {}
//Dereference operator
int operator*() const {
return fib;
}
//Generate next element
InfiniteFibonacciSequence& operator++() {
n1 = n2;
n2 = fib + n2;
fib = n1;
return *this;
}
private:
int n1;
int n2;
int fib;
};
int main() {
InfiniteFibonacciSequence num;
// Print the first 20 Fibonacci numbers
for (int i = 0; i < 20; ++i) {
std::cout << *num << " ";
++num;
}
//Output: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181
return 0;
}

In the code above, we create a class named InfiniteFibonacciSequence with data members, a dereferencing operator, and an increment operator. This is more complex compared to the ease provided in C++20, with ranges and lazy evaluation.

Note: It is important to note the difference between an infinite sequence and an infinite loop. In an infinite sequence, the focus is on the generation of the next element without any termination value. However, an infinite loop focuses on the continuously repeated execution of some lines of code with a termination condition.

Conclusion#

In short, the ranges library provides a more readable and maintainable way to perform operations on a collection of data. Its lazy evaluation, the possibility of composition, and the nonrequirement of extra memory for the view makes it more practical for many scenarios.

We hope that this blog has sparked your interest in learning C++20. For further reading, please continue with the following courses:

The All-in-One Guide to C++20 introduces C++20 in great detail.

C++ Fundamentals for Professionals is for refreshing your knowledge of C++.

Grokking Coding Interview Patterns in C++ to help you master C++ interviews.


Written By:
Syed Atif Mehdi
Join 2.5 million developers at
Explore the catalog

Free Resources