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.
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.
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.
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: 902return 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 vectorauto max_element = std::max_element(v.begin(), v.end());std::cout << *max_element << std::endl; // Output: 902return 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.
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.
In the following code, we define a vector of integers and call it v
.
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.
The resulting view even_numbers
represents a sequence of even numbers from the vector v
.
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 evaluationauto 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 10return 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 10return 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.
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 "|" operatorauto 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 1for (int num : result) {std::cout << num << " ";}std::cout << std::endl; //29 25 21 17 13return 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 numbersstd::copy_if(numbers.begin(), numbers.end(), std::back_inserter(even_numbers), [](int x) { return x % 2 == 0; });// Double each numberstd::transform(even_numbers.begin(), even_numbers.end(), even_numbers.begin(), [](int x) { return x * 2; });// Reverse the orderstd::reverse(even_numbers.begin(), even_numbers.end());// Take the first 5 elementsif (even_numbers.size() > 5) {even_numbers.resize(5);}// Increment each element by 1for (int& num : even_numbers) {num += 1;}for (int num : even_numbers) {std::cout << num << " ";}std::cout << std::endl; //29 25 21 17 13return 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.
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 numbersauto 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 numbersfor (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 4181return 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 variablesInfiniteFibonacciSequence() : n1(0), n2(1), fib(0) {}//Dereference operatorint operator*() const {return fib;}//Generate next elementInfiniteFibonacciSequence& 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 numbersfor (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 4181return 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.
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.
Free Resources