Modern C++ Ranges (TS/20/v3), STL 2.0 or hype?

When template syntax (generics) first appeared in C++ some people were skeptical about whether it was either useful or practical, while others managed to determine that a Turing-complete compile-time language had been inadvertently created. It must be difficult to imagine C++ without std::vector<T>, and similar; the application which surely sold templates was the STL (Standard Template Library), which became part of the first C++ Standard (C++98) when such support in contemporary programming languages was limited.

Ranges are an attempt to simplify the composition of several actions on a range of input values, usually of the same type (such as int). The Ranges TS added a part of the third-party library of the same name to the Standard, this becoming part of C++20. However the freely available “range-v3” library contains much functionality not (yet) found in the Standard, and being header-only means there is little reason to not use it. (Two of the four examples in this article compile with C++20 supporting compilers, the other two need range-v3.)

To start off let’s look at a simple program which copies from std::cin to std::cout:

#include <ranges>
#include <iostream>

using namespace std::ranges;

int main() {
    std::cout << "Type some integers:\n";
    auto pipeline = istream_view<int>(std::cin)
        | views::take(6);
    for (auto n : pipeline) {
        std::cout << n << '\n';
    }
}

Some important features to note are:

  • The variable pipeline has two stages, separated by a vertical pipe (|) symbol.
  • It is defined with auto, its type being essentially opaque.
  • The infinite range created by the first stage is made finite (six elements long) by the second.
  • It can be iterated over using a range-for loop.
  • When running this program, input and output are interleaved; ranges are lazy.

Range views are similar to std::string_view and std::span<T> in that they mostly do now “own” the whole data that they reference, each working on only one element at a time. A major difference is that ranges of infinite data can be manipulated, up to a point (such as when it is desired to place a finite amount of data into a container).

A second example outputs the first ten multiples of 13, interestingly stages two and three of this pipeline can be written in either order with little change in resource usage or functionality:

#include <ranges>
#include <iostream>

using namespace std::ranges;

int main() {
    auto pipeline = views::iota(1)
        | views::transform([](int n){ return n * 13; })
        | views::take(10);
    for (auto n : pipeline) {
        std::cout << n << ' ';
    }
    std::cout << '\n';
}

The view generator views::iota works in a similar way to the Standard Library iota algorithm, producing a sequence of integer numbers which increase by one on every iteration. In this variant with one value for the start of the sequence, it is unlimited. The views::transform stage takes a function as its parameter, in this case a lambda defined inline which multiples its input by 13. Then as before views::take restricts the pipeline to ten items before output.

A more complex example where the number of items at the end of the pipeline is less than at the beginning (items can only be dropped, or filtered out, by intermediate stages, not added) is shown below:

#include <range/v3/view.hpp>
#include <range/v3/numeric/accumulate.hpp>
#include <iostream>
#include <vector>

using namespace ranges::v3;

int main() {
    std::vector<unsigned long long> primes{};
    auto is_prime = [&primes](auto n){
        for (auto p : primes) {
            if (n % p == 0) {
                return false;
            }
            if (p * p > n) {
                break;
            }
        }
        primes.push_back(n);
        return true;
    };

    auto pipeline = views::iota(2)
        | views::filter(is_prime)
        | views::take(1'000'000);
    std::cout << "Sum of the first million primes is: " << accumulate(pipeline, unsigned long long{}) << '\n';
}

The views::filter stage takes a predicate function as its parameter, exactly the same as for std::copy_if and similar algorithms from the Standard Library. The range-v3 function accumulate takes a range and a starting value, unlike std::accumulate which takes a start and end iterator with a starting value. Other functions which operate on ranges are available.

Finally, onto an example which populates a std::vector from an input stream of random numbers filtered to be divisible by 101 (itself a prime number):

#include <range/v3/view.hpp>
#include <random>
#include <vector>
#include <iostream>

using namespace ranges::v3;

int main() {
    std::mt19937 my_rand(0);
    auto pipeline = views::generate(my_rand)
        | views::take(1'000'000)
        | views::filter([](auto n){ return (n % 101) == 0; })
        | to<std::vector>();
    std::cout << "Expected: " << 1'000'000 / 101 << ", got: " << pipeline.size() << '\n';
}

The method shown in this example could be used to determine the quality of a random-number generator; views::generate calls my_rand() (a function object) to get the next random number, of which a million are taken and filtered. The size of the std::vector is the number of random numbers which matched.

So, are Ranges really STL 2.0, as some have claimed? I’m becoming more convinced that they are, providing as they do true functional-language capability to C++. Interestingly, the <ranges> header in MSVC requires /std:c++latest, meaning that C++ Concepts and C++20 support are available. (The publicly available range-v3 needs only C++17 support.) Just as the original STL could only be implemented with support for generic programming being available, Ranges really need Concepts in order to be release grade in providing sane error messages when creating an invalid chain. Look forward to a greater part of range-v3 in C++23, by which time more people might be calling it STL 2.0 (think about the effort involved in converting all these examples to use traditional container-manipulating algorithms).

Resources: Download or browse the source code

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s