跳转至

Ranges(范围)

title: C++20ranges 有 3 个特点
1. **Direct on the Container(直接操作容器 )** 
2. **Lazy Evaluation(惰性计算)**
3. **Function Composition (函数组合)**(重载`|`,以管道方式连接范围库相关函数)

1 Direct on the Container

The classical algorithms of the Standard Template Library (STL) are sometimes a little inconvenient. They need a begin and an end iterator. This is often more than you want to write.

// sortClassical.cpp
#include <algorithm>
#include <iostream>
#include <vector>
int main()  {
    std::vector<int> myVec{-3, 5, 0, 7, -4};
    std::sort(myVec.begin(), myVec.end());     // (1)
    for (auto v: myVec) std::cout << v << " "; // -4, -3, 0, 5, 7
}

Wouldn't it be nice if std::sort (line 1) could be executed on the entire container? Thanks to the ranges library, this is possible in C++20.

// sortRanges.cpp
#include <algorithm>
#include <iostream>
#include <vector>
int main()  {
    std::vector<int> myVec{-3, 5, 0, 7, -4};
    std::ranges::sort(myVec);                  // (1)
    for (auto v: myVec) std::cout << v << " "; // -4, -3, 0, 5, 7
}

std::ranges::sort (line 1) operates directly on the container.

2 Lazy Evaluation

std::views::iota is a range factory for creating a sequence of elements by successively incrementing an initial value. This sequence can be finite or infinite. The elements of the range factory are only created when needed.

// lazyRanges.cpp
#include <iostream>
#include <ranges>
int main() {
    std::cout << "\n";
    for (int i: std::views::iota(1'000'000, 1'000'010)) {     // (1)
        std::cout << i << " ";  
    }
    std::cout << "\n\n";

    for (int i: std::views::iota(1'000'000)                   // (2)
                | std::views::take(10)) {
        std::cout << i << " ";  
    }
    std::cout << "\n\n";
    for (int i: std::views::iota(1'000'000)                  // (3)
                | std::views::take_while([](auto i) { return i < 1'000'010; } )) {
        std::cout << i << " ";  
    }
    std::cout << "\n\n";
}

The small program shows the difference between creating a finite data stream (line 1), and an infinite data stream (lines 2, and 3). When you create an infinite data stream, you need a boundary condition. Line (2) uses the view std::views::take(10), and line 3 the view std::views::take_while.std::views::take_while requires a predicate. This is an ideal fit for a lambda expression: [](auto i) { return i < 1'000'010; }.Since C++14, you can use single quotes as separators in integer literals: 1'000'010. All three std::views::ranges calls produce the same numbers. lazyRanges I already used the pipe operator (|) in this example for function composition. Let's go one step further.

3 Function Composition

The following program primesLazy.cpp creates the first ten prime numbers starting with one million.

// primesLazy.cpp
#include <iostream>
#include <ranges>
bool isPrime(int i) {
    for (int j=2; j*j <= i; ++j){
        if (i % j == 0) return false;
    }
    return true;
}
int main() { 
    std::cout << '\n';

    auto odd = [](int i){ return i % 2 == 1; };
    for (int i: std::views::iota(1'000'000) | std::views::filter(odd) 
                                            | std::views::filter(isPrime) 
                                            | std::views::take(10)) {
        std::cout << i << " ";  
    }
    std::cout << '\n';
}

You have to read the function composition from left to right: I create an infinite data stream starting with 1'000'000 (std::views::iota(1'000'000)) and apply two filters. Each filter needs a predicate. The first filter let the odd element pass (std::views::filter(odd)), and the second filter let the prime numbers pass (std::views::filter(isPrime)). To end the infinite data stream, I stop after 10 numbers (std::views::take(10)). Finally, here are the first ten prime numbers starting with one million. primesLazy You may ask: Who starts the processing of this data pipeline? Now, it goes from right to left. The data sink (std::views::take(10)) want to have the next value and ask, therefore, its predecessor. This request goes on until the range-based for-loop as the data source can produce one number. This was my short recap. When you want to read more about the ranges library, read my previous posts: - The Ranges Library - Functional Pattern with the Ranges Library - Pythonic with the Ranges Library - Pythons range Function, the Second - Pythons map Function Now, it's time to write about something new.

4 std Algorithms versus std::ranges Algorithms

The algorithms of the algorithm library and the memory libray have ranges pendants. They start with the namespace std::ranges. The numeric library does not have a ranges pendant. Now, you may have the question: Should I use the classical std algorithm or the new std::ranges algorithm. Let me start with a comparison of the classical std::sort and the new std::ranges::sort. First, here are the various overloads of std::sort and std::ranges::sort.

template< class RandomIt >
constexpr void sort( RandomIt first, RandomIt last );
template< class ExecutionPolicy, class RandomIt >
void sort( ExecutionPolicy&& policy,
           RandomIt first, RandomIt last );
template< class RandomIt, class Compare >
constexpr void sort( RandomIt first, RandomIt last, Compare comp );
template< class ExecutionPolicy, class RandomIt, class Compare >
void sort( ExecutionPolicy&& policy,
           RandomIt first, RandomIt last, Compare comp );

std::sort has four overloads in C++20. Let's see what I can deduce from the names of the function declarations. All four overloads take a range, given by a begin and end iterator. The iterators must be a random access iterators. The first and third overloads are declared as constexpr and can, therefore, run at compile time. The second and fourth overload require an [execution policy](https://www.modernescpp.com/(https:/www.modernescpp.com/index.php/parallel-algorithms-of-the-stl-with-gcc). The execution policy lets you specify if the program should run sequential, parallel, or vectorized.

Additionally, the last two overloads lines 8 and 11 let you specify the sorting strategy. Compare has to be a binary predicate. A binary predicate is a callable that takes two arguments and returns something convertible to a bool.

I assume my analysis reminded you of concepts. But there is a big difference. The names in the std::sort do not stand for concepts but only for documentation purposes. In std::ranges::sort the names are concepts.

template <std::random_access_iterator I, std::sentinel_for<I> S,
         class Comp = ranges::less, class Proj = std::identity>
requires std::sortable<I, Comp, Proj>
constexpr I sort(I first, S last, Comp comp = {}, Proj proj = {});
template <ranges::random_access_range R, class Comp = ranges::less, 
          class Proj = std::identity>
requires std::sortable<ranges::iterator_t<R>, Comp, Proj>
constexpr ranges::borrowed_iterator_t<R> sort(R&& r, Comp comp = {}, Proj proj = {});

When you study the two overloads, you notice that it takes a sortable range R (lines 2 and 4), either given by a begin iterator and end sentinel (line 1) or by a ranges::random_access_range (line 3). The iterator and the range must support random access. Additionally, the overloads take a predicate Comp, and a projection Proj. The predicate Comp uses by default ranges::less, and the projection Proj the identity std::identity. A projection is a mapping of a set into a subset. In C++20, std::ranges::sort does not support execution policies.