Post

C++20范围库

1.引言

C++20引入的范围(Ranges)库是对STL算法库的扩展,使算法和迭代器可以组合,从而使其功能更加强大。范围库为函数式编程提供了更好的支持。

例如,下面的示例程序输出一个整数数组中所有偶数的平方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <ranges>

int main() {
    int arr[] = {0, 1, 2, 3, 4, 5};
    auto even = [](int i) { return i % 2 == 0; };
    auto square = [](int i) { return i * i; };

    // the "pipe" syntax of composing the views:
    for (int i : arr | std::views::filter(even) | std::views::transform(square))
        std::cout << i << ' ';
    std::cout << '\n';

    // a traditional "functional" composing syntax:
    for (int i : std::views::transform(std::views::filter(arr, even), square))
        std::cout << i << ' ';
    return 0;
}

程序输出如下:

1
2
0 4 16 
0 4 16 

本文将介绍范围、视图等基本概念,以及范围算法相比于传统STL算法的优势,最后将介绍如何自定义视图。

注:支持范围库的编译器最低版本是GCC 10和Clang 13,编译时需要添加选项-std=c++20。参见C++ compiler support

2.范围

范围(range)是对可迭代序列的抽象,即提供了begin()end()迭代器、允许对其元素进行迭代的类型。内置数组和所有STL容器都是范围。

范围可用于foreach循环,或者传递给范围算法。例如,头文件<algorithm>中的算法std::ranges::sort()可以对给定的范围进行排序:

1
2
3
std::vector<int> v = ...;
std::ranges::sort(v);  // with C++20
std::sort(v.begin(), v.end());  // before C++20

大部分传统的STL算法接受一对迭代器,而范围算法可以将容器作为单个参数传递。范围算法将在2.2节详细介绍。

为了简单起见,下面将命名空间std::ranges简称为ranges

2.1 范围概念

头文件<ranges>中的概念ranges::range给出了“范围”的定义:

1
2
3
4
5
template<class T>
concept range = requires(T& t) {
    ranges::begin(t);
    ranges::end(t);
};

简单来说,范围就是可以传递给函数ranges::begin()ranges::end()的类型。这两个函数的定义如下:

  • 如果t是数组类型T[N],则分别返回t + 0t + N
  • 否则,返回t.begin()t.end(),如果T定义了该成员函数,且返回类型是输入或输出迭代器(std::input_or_output_iterator,即支持*++==!=操作)。例如STL容器。
  • 否则,分别返回begin(t)end(t),如果该表达式合法且返回类型是输入或输出迭代器。
  • 否则,该函数调用是非法的。

注:

  • 范围的end()迭代器(也叫做哨兵(sentinel))类型不必和begin()相同。例如,可以用'\0'结束一个C风格字符串,空字符串结束一个单词列表,空指针结束一个链表,或者-1结束一个非负数列表。
  • 在C++20之前,标准库还有一对类似的函数std::begin()std::end(),定义在各容器对应的头文件中。二者的区别详见What is the difference between std::ranges::begin and std::begin?
  • 头文件<ranges>中还定义了其他的范围函数,如ranges::size()ranges::empty()等。
  • 概念concept也是C++20的新特性,详见《C++20之概念》

根据其迭代器类别,范围也可以分为不同的类别,分类方式与迭代器类似:

范围概念迭代器概念
ranges::input_rangestd::input_iterator
ranges::output_rangestd::output_iterator
ranges::forward_rangestd::forward_iterator
ranges::bidirectional_rangestd::bidirectional_iterator
ranges::random_access_rangestd::random_access_iterator
ranges::contiguous_rangestd::contiguous_iterator

可以使用上述概念检查一个类是否是(某种类别的)范围。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <forward_list>
#include <map>
#include <ranges>
#include <string>
#include <vector>

// A minimum range
struct SimpleRange {
    int* begin();
    int* end();
};

// Not a range: no begin/end
struct NotRange {
    int t;
};

// Not a range: begin does not return an input_or_output_iterator
struct NotRange2 {
    void* begin();
    int* end();
};

static_assert(std::ranges::random_access_range<int[10]>);
static_assert(std::ranges::random_access_range<std::vector<int>>);
static_assert(std::ranges::random_access_range<std::string>);
static_assert(std::ranges::bidirectional_range<std::map<int, int>>);
static_assert(std::ranges::forward_range<std::forward_list<double>>);
static_assert(std::ranges::range<SimpleRange>);

static_assert(!std::ranges::range<int[]>);
static_assert(!std::ranges::range<NotRange>);
static_assert(!std::ranges::range<NotRange2>);

2.2 范围算法

范围库为头文件<algorithm>中的大多数算法提供了基于范围的版本,定义在命名空间std::ranges中。例如findcountsortfor_each等。完整列表参见Constrained algorithms

与传统STL算法相比,范围算法有以下优点,从而使用起来更加灵活:

  • 可以通过一对迭代器或单个参数指定范围。
  • 支持元素映射和成员指针。
  • 改变了大多数算法的返回类型,以返回可能有用的信息。

下面以几个常用的算法为例介绍范围算法的用法。

2.2.1 sort

首先考虑sort算法。传统的std::sort()有以下两种常用形式:

1
2
3
4
5
template<class RandomIt>
void sort(RandomIt first, RandomIt last);

template<class RandomIt, class Compare>
void sort(RandomIt first, RandomIt last, Compare comp);

第一种形式使用<[first, last)中的元素进行排序,第二种形式使用自定义比较器comp

范围库提供的ranges::sort()有以下两种形式:

1
2
3
4
5
6
7
template<std::random_access_iterator I, std::sentinel_for<I> S,
    class Comp = ranges::less, class Proj = std::identity>
sort(I first, S last, Comp comp = {}, Proj proj = {});

template<ranges::random_access_range R,
    class Comp = ranges::less, class Proj = std::identity>
sort(R&& r, Comp comp = {}, Proj proj = {});

第一种形式等价于std::sort(first, last, comp),但额外提供了proj参数,用于对元素进行映射操作,即对映射后的结果而不是元素本身进行排序(相当于Python中sort()函数的key参数),默认为恒等映射std::identity。第二种形式等价于ranges::sort(ranges::begin(r), ranges::end(r), comp, proj)

下面的程序展示了sort算法的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

namespace ranges = std::ranges;

void print(const std::string& comment, const auto& seq, char sep = ' ') {
    for (std::cout << comment << '\n'; const auto& e : seq)
        std::cout << e << sep;
    std::cout << '\n';
}

struct Particle {
    std::string name;
    double mass; // MeV
};

std::ostream& operator<<(std::ostream& os, const Particle& p) {
    return os << std::left << std::setw(8) << p.name << " : " << p.mass;
}

int main() {
    std::vector<int> v = {5, 7, 4, 2, 8, 6, 1, 9, 0, 3};
    ranges::sort(v);
    print("Sort using operator<", v);

    ranges::sort(v, ranges::greater{});
    print("Sort using ranges::greater", v);

    Particle particles[] {
        {"Electron", 0.511}, {"Muon", 105.66}, {"Tau", 1776.86},
        {"Positron", 0.511}, {"Proton", 938.27}, {"Neutron", 939.57}
    };
    ranges::sort(particles, {}, &Particle::name);
    print("\nSort by name", particles, '\n');

    ranges::sort(particles, ranges::greater{}, &Particle::mass);
    print("Sort by mass in descending order", particles, '\n');
    return 0;
}

程序输出如下(https://godbolt.org/z/Er4sadWzq):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Sort using operator<
0 1 2 3 4 5 6 7 8 9 
Sort using ranges::greater
9 8 7 6 5 4 3 2 1 0 

Sort by name
Electron : 0.511
Muon     : 105.66
Neutron  : 939.57
Positron : 0.511
Proton   : 938.27
Tau      : 1776.86

Sort by mass in descending order
Tau      : 1776.86
Neutron  : 939.57
Proton   : 938.27
Muon     : 105.66
Electron : 0.511
Positron : 0.511

2.2.2 find_if

find_if算法用于查找满足给定条件的元素。传统的std::find_if()实现如下:

1
2
3
4
5
6
7
template<class InputIt, class Pred>
InputIt find_if(InputIt first, InputIt last, Pred pred) {
    for (; first != last; ++first)
        if (pred(*first))
            return first;
    return last;
}

ranges::find_if()有以下两种形式(忽略复杂的模板参数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<std::input_iterator I, std::sentinel_for<I> S, class Proj = std::identity,
         std::indirect_unary_predicate<std::projected<I, Proj>> Pred>
I find_if(I first, S last, Pred pred, Proj proj = {}) {
    for (; first != last; ++first)
        if (std::invoke(pred, std::invoke(proj, *first)))
            return first;
    return first;
}

template<ranges::input_range R, class Proj = std::identity,
         std::indirect_unary_predicate<std::projected<ranges::iterator_t<R>, Proj>> Pred>
ranges::borrowed_iterator_t<R> find_if(R&& r, Pred pred, Proj proj = {}) {
    return find_if(ranges::begin(r), ranges::end(r), std::ref(pred), std::ref(proj));
}

传统STL算法的谓词参数可以是普通函数、Lambda表达式或函数对象,但不能是成员指针(除非使用std::mem_fn包装)。这是由于成员指针的调用语法与其他函数对象不同(使用.*->*而不是())。而范围算法都支持。

从上面的实现中可以看出,std::find_if()直接使用()运算符来调用谓词,这不适用于成员指针;而ranges::find_if()使用了std::invoke(),该函数为成员指针和其他函数对象提供了统一的调用语法。参见《C++函数式编程》 6.2.2节和6.3节“注”。

下面的程序展示了find_if的使用示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

namespace ranges = std::ranges;

struct User {
    std::string name;
    int age;
    bool is_adult() const { return age >= 18; }
};

std::ostream& operator<<(std::ostream& os, const User& u) {
    return os << '(' << u.name << ',' << u.age << ')';
}

class Larger_than {
public:
    explicit Larger_than(int v) :v(v) {}
    bool operator()(int x) const { return x > v; }

private:
    int v;
};

int main() {
    std::vector<int> v = {4, 1, 3, 2};
    for (int n : {3, 5}) {
        if (ranges::find(v, n) != v.end())
            std::cout << "v contains " << n << '\n';
        else
            std::cout << "v does not contain " << n << '\n';
    }

    auto is_even = [](int x) { return x % 2 == 0; };
    if (auto it = ranges::find_if(v, is_even); it != v.end())
        std::cout << "First even element in v: " << *it << '\n';
    else
        std::cout << "No odd element in v\n";

    if (auto it = ranges::find_if(v, Larger_than(42)); it != v.end())
        std::cout << "First element larger than 42 in v: " << *it << '\n';
    else
        std::cout << "No element larger than 42 in v\n";

    User users[] = { {"Alice", 17}, {"Bob", 25}, {"Cindy", 19}, {"Dale", 20}, {"Eric", 22} };
    if (auto it = ranges::find_if(users, &User::is_adult); it != ranges::end(users))
        std::cout << "First adult in users: " << *it << '\n';
    else
        std::cout << "No adult in users\n";

    std::string s = "Frank";
    if (auto it = ranges::find(users, s, &User::name); it != ranges::end(users))
        std::cout << "Found " << s << " in users: " << *it << '\n';
    else
        std::cout << "Not found " << s << " in users\n";
    return 0;
}

程序输出如下(https://godbolt.org/z/61sre5c61):

1
2
3
4
5
6
v contains 3
v does not contain 5
First even element in v: 4
No element larger than 42 in v
First adult in users: (Bob,25)
Not found Frank in users

其中,查找成年用户的例子使用了成员函数指针&User::is_adult作为谓词,而std::find_if()只能使用lambda表达式或std::mem_fn

1
2
auto p = std::find_if(users.begin(), users.end(), [](const User& u) { return u.is_adult(); });
auto q = std::find_if(users.begin(), users.end(), std::mem_fn(&User::is_adult));

2.2.3 for_each

std::for_each()算法对[first, last)中的每个元素调用函数f

1
2
3
4
5
6
template<class InputIt, class Fun>
Fun for_each(InputIt first, InputIt last, Fun f) {
    for (; first != last; ++first)
        f(*first);
    return f;
}

算法最终返回f,这对于有状态的函数对象可能会有用(例如下面示例中的Sum)。

对应的ranges::for_each()实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<std::input_iterator I, std::sentinel_for<I> S, class Proj = std::identity,
         std::indirectly_unary_invocable<std::projected<I, Proj>> Fun>
ranges::for_each_result<I, Fun> for_each(I first, S last, Fun f, Proj proj = {}) {
    for (; first != last; ++first)
        std::invoke(f, std::invoke(proj, *first));
    return {std::move(first), std::move(f)};
}

template<ranges::input_range R, class Proj = std::identity,
         std::indirectly_unary_invocable<std::projected<ranges::iterator_t<R>, Proj>> Fun>
ranges::for_each_result<ranges::borrowed_iterator_t<R>, Fun> for_each(R&& r, Fun f, Proj proj = {}) {
    return for_each(ranges::begin(r), ranges::end(r), std::move(f), std::ref(proj));
}

除了增加映射参数和改变调用语法外,ranges::for_each()还返回了first迭代器的最终位置。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <algorithm>
#include <cassert>
#include <iostream>
#include <string>
#include <utility>
#include <vector>

namespace ranges = std::ranges;

struct Sum {
    void operator()(int n) { sum += n; }
    int sum = 0;
};

int main() {
    std::vector<int> nums = {3, 4, 2, 8, 15, 267};
    auto print = [](const auto& n) { std::cout << n << ' '; };

    std::cout << "before: ";
    ranges::for_each(nums, print);

    ranges::for_each(nums, [](int& n) { ++n; });

    // calls Sum::operator() for each number
    auto [i, s] = ranges::for_each(nums, Sum{});
    assert(i == nums.end());

    std::cout << "\nafter: ";
    ranges::for_each(nums, print);
    std::cout << "\nsum: " << s.sum << '\n';

    using pair = std::pair<int, std::string>;
    std::vector<pair> pairs = { {1, "one"}, {2, "two"}, {3, "three"} };

    std::cout << "project the pair::first: ";
    ranges::for_each(pairs, print, [](const pair& p) { return p.first; });

    std::cout << "\nproject the pair::second: ";
    ranges::for_each(pairs, print, &pair::second);
    std::cout << '\n';
    return 0;
}

程序输出如下(https://godbolt.org/z/TedWvjYYh):

1
2
3
4
5
before: 3 4 2 8 15 267 
after: 4 5 3 9 16 268 
sum: 305
project the pair::first: 1 2 3 
project the pair::second: one two three 

3.视图

视图(view)是间接表示范围的轻量级对象。视图不拥有数据,而是在迭代时惰性计算或生成元素,其拷贝、移动和赋值等操作具有常数时间复杂度。

标准库提供的视图定义在头文件<ranges>中,主要包括两大类:

  • 范围工厂:在迭代时生成元素,如ranges::iota_view
  • 范围适配器:对底层范围的包装器,在迭代时进行惰性计算,如ranges::filter_view

视图是一种特殊的范围,因此也可用于foreach循环或者传递给范围算法。STL容器是范围,但不是视图。

注:C++中的范围和视图可分别类比为Python中的可迭代对象(iterable)和生成器(generator)。

3.1 范围工厂

范围工厂(range factory)是在迭代时生成元素的视图。

例如,视图ranges::iota_view用于生成连续的整数序列(类似于Python的range()函数):

  • ranges::iota_view(m, n)生成有限序列m, m+1, …, n-1(注意,如果n < m则迭代不会终止)
  • ranges::iota_view(n)生成无限序列n, n+1, …(因此不能直接对其迭代)

大多数视图都有一个对应的函数,定义在命名空间std::ranges::views中,用于创建这种视图。例如,ranges::views::iota(m, n)等价于ranges::iota_view(m, n)ranges::views::iota(n)等价于ranges::iota_view(n)

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <algorithm>
#include <iostream>
#include <ranges>

int main() {
    for (int i : std::ranges::iota_view(1, 10))
        std::cout << i << ' ';
    std::cout << '\n';

    for (int i : std::views::iota(1, 10))
        std::cout << i << ' ';
    std::cout << '\n';

    for (int i : std::views::iota(1) | std::views::take(9))
        std::cout << i << ' ';
    std::cout << '\n';

    auto print = [](int i){ std::cout << i << ' '; };
    std::ranges::for_each(std::views::iota(1, 10), print);
    std::cout << '\n';
    return 0;
}

输出如下(https://godbolt.org/z/x1Gjzscre):

1
2
3
4
1 2 3 4 5 6 7 8 9 
1 2 3 4 5 6 7 8 9 
1 2 3 4 5 6 7 8 9 
1 2 3 4 5 6 7 8 9 

范围工厂的完整列表参见Range factories

命名空间别名std::viewsstd::ranges::views的简写。下面将其简称为views

3.2 范围适配器

范围适配器(range adaptor)是对底层范围的包装器,在迭代时进行惰性计算(如过滤、映射等),并且支持通过管道运算符(|)将多个操作串联起来(就像UNIX Shell中的管道)。

范围算法是立即执行的,可能会修改范围中的元素;而范围适配器是惰性计算的,会返回一个新视图,不会修改原始数据。

例如,回看本文开头打印偶数平方的例子。表达式

1
arr | views::filter(even) | views::transform(square)

返回一个新的视图,该视图是由arr中的元素经过谓词even过滤、再经过函数square转换构成的。另外,范围适配器还支持函数调用语法,上面的表达式等价于

1
views::transform(views::filter(arr, even), square)

这种表达式的返回类型是某种视图,具体类型并不重要,只需知道可以直接对其迭代、传递给另一个视图或者传递给范围算法。如果需要将中间结果保存到变量中,可以使用auto声明。

对比上述两种形式会发现,范围适配器有两种调用方式:接受一个范围参数和额外参数(如views::filter(arr, even)),或者只有额外参数(如views::filter(even))。要弄清底层实现原理,需要了解两个基本概念:范围适配器对象和范围适配器闭包对象。

范围适配器闭包对象(range adaptor closure object)是接受一个范围参数、返回一个视图的函数对象(详见具名要求RangeAdaptorClosureObject和概念ranges::range_adaptor_closure)。范围参数可以通过管道语法(|)或函数调用语法(())传递。假设c是一个范围适配器闭包对象,r是一个范围,则以下两个表达式等价:

1
2
c(r)
r | c

两个范围适配器闭包对象可以用管道运算符|连接起来,以产生另一个范围适配器闭包对象。假设cd是范围适配器闭包对象,c | d产生范围适配器闭包对象er是一个范围,则以下表达式等价:

1
2
3
4
d(c(r))
r | c | d
e(r)   // (c | d)(r)
r | e  // r | (c | d)

范围适配器对象(range adaptor object)是接受一个范围作为第一个参数(可能还有额外参数)、返回一个视图的函数对象(详见具名要求RangeAdaptorObject)。

  • 如果一个范围适配器对象只接受一个参数,则它也是一个范围适配器闭包对象。
  • 如果一个范围适配器对象接受多个参数,则它也支持部分应用(partial application)。假设a是一个范围适配器对象,r是一个范围,args...是额外参数,则表达式a(args...)是一个范围适配器闭包对象,以下表达式等价:
1
2
3
a(r, args...)
a(args...)(r)
r | a(args...)

例如,范围适配器views::filter接受一个范围参数和一个谓词参数。在前面的例子中,views::filter(arr, even)是一个视图,而views::filter(even)是一个范围适配器闭包对象。以下三种形式是等价的:

1
2
3
views::filter(arr, even)
views::filter(even)(arr)
arr | views::filter(even)

组合视图时甚至可以混合使用不同的形式,例如:

1
2
3
4
5
views::transform(views::filter(arr, even), square)
arr | views::filter(even) | views::transform(square)
(views::filter(even) | views::transform(square))(arr)
views::transform(arr | views::filter(even), square)
...

范围适配器的完整列表参见Range adaptors

常用的视图及其等价的Python函数如下表所示。

视图等价的Python函数
views::iota(m, n)range(m, n)
views::iota(n)itertools.count(n)
views::empty<T> 
views::single(e) 
views::repeat(e, n)itertools.repeat(e, n)
views::repeat(e)itertools.repeat(e)
views::filter(r, p)filter(p, r)
views::transform(r, f)map(f, r)
views::take(r, n)itertools.islice(r, n)
views::take_while(r, p)itertools.takewhile(p, r)
views::drop(r, n)itertools.islice(r, n, None)
views::drop_while(r, p)itertools.dropwhile(p, r)
views::concat(rs...)itertools.chain(rs...)
views::join(r)itertools.chain.from_iterable(r)
views::reverse(r)reversed(r)
views::keysmap(operator.itemgetter(0), r)
views::valuesmap(operator.itemgetter(1), r)
views::elements<N>(r)map(operator.itemgetter(N), r)
views::enumerate(r)enumerate(r)
views::zip(rs...)zip(rs...)
views::chunk(r, n)itertools.batched(r, n)
views::stride(r, n)itertools.islice(r, None, None, n)

另外,还有两个表示子范围的视图:

  • ranges::subrange(first, last) 生成表示范围[first, last)的视图
  • views::counted(it, n) 生成表示范围[it, it + n)的视图

3.3 基于范围的容器操作

C++23为STL容器增加了基于范围的操作(需要GCC 14+或Clang 17+)。

可以使用ranges::to<C>将范围转换为容器C。例如:

1
2
3
4
5
6
7
auto vec = std::views::iota(1, 5)
        | std::views::transform([](int i) { return i * 2; })
        | std::ranges::to<std::vector>();
std::println("{}", vec);  // prints "[2, 4, 6, 8]"

auto list = vec | std::views::take(3) | std::ranges::to<std::list<double>>();
std::println("{}", list);  // prints "[2, 4, 6]"

容器还增加了基于范围的插入操作。例如,对于向量v

  • v.insert_range(p, r)将范围r中的元素插入p之前,等价于v.insert(p, r.begin(), r.end())
  • v.append_range(r)将范围r中的元素添加到向量末尾,等价于v.insert(v.end(), r.begin(), r.end())

3.4 示例

本节给出一些视图的使用示例。

视图的元素类型取决于底层范围和执行的动作。如果直接对数组或容器使用views::filter,得到的视图的元素是底层范围元素的引用,因此可以通过视图修改原始数据。例如:

1
2
3
4
5
6
7
int input[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto even = [](int i) { return i % 2 == 0; };
auto even_nums = input | std::views::filter(even);
std::ranges::fill(even_nums, 42);
for (int i : input)
    std::cout << i << ' ';
// prints "42 1 42 3 42 5 42 7 42 9 42"

views::keysviews::values分别提取元组式值的第一个和第二个元素。元组式(tuple-like)值包括std::pairstd::tuplestd::array等。这两个视图可用于映射或std::pair向量等。例如,可以如下从映射中提取键和值:

1
2
3
4
5
6
7
8
9
10
11
std::map<char, int> m = { {'A', 1}, {'B', 2}, {'C', 3}, {'D', 4}, {'E', 5} };
for (char k : m | std::views::keys | std::views::transform(tolower))
    std::cout << k << ' ';
// prints "a b c d e"
std::cout << '\n';

auto odd = [](int i) { return i % 2 == 1; };
for (int v : m | std::views::values | std::views::filter(odd))
    std::cout << v << ' ';
// prints "1 3 5"
std::cout << '\n';

views::split使用给定的分隔符将范围分割成子范围。例如,可以如下使用std::string_view分割字符串:

1
2
3
4
5
6
7
8
using std::operator""sv;
auto words = "Hello^_^C++^_^20^_^!"sv;
auto delim = "^_^"sv;
for (const auto word : std::views::split(words, delim))
    // with string_view's C++23 range constructor:
    std::cout << std::quoted(std::string_view(word)) << ' ';
// prints: "Hello" "C++" "20" "!" 
std::cout << '\n';

views::join打平(flatten)嵌套范围。例如,可以如下拼接字符串和向量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using namespace std::literals;

const auto bits = {"https:"sv, "//"sv, "cppreference"sv, "."sv, "com"sv};
for (char c : bits | std::views::join)
    std::cout << c;
// prints "https://cppreference.com"
std::cout << '\n';

std::vector<std::vector<int>> v = { {1, 2}, {3, 4, 5}, {6}, {7, 8, 9} };
auto jv = v | std::views::join | std::ranges::to<std::vector>();
for (int e : jv)
    std::cout << e << ' ';
// prints "1 2 3 4 5 6 7 8 9"
std::cout << '\n';

views::enumerate(C++23引入)将每个元素映射为(索引,值)对(类似于Python的enumerate()函数)。例如:

1
2
3
4
5
6
7
8
9
10
11
std::string s = "ABCD";
for (auto [i, c] : std::views::enumerate(s))
    std::cout << '(' << i << ':' << c << ") ";
// prints "(0:A) (1:B) (2:C) (3:D)"
std::cout << '\n';

auto m = s | std::views::enumerate | std::ranges::to<std::map>();
for (auto [k, v] : m)
    std::cout << '[' << k << "]:" << v << ' ';
// prints "[0]:A [1]:B [2]:C [3]:D"
std::cout << '\n';

views::zip(C++23引入)生成由每个范围对应位置元素的元组构成的视图,到最短的范围结束为止(类似于Python的zip()函数)。例如:

1
2
3
4
5
std::vector a = {1, 2, 3, 4};
std::array b = {'A', 'B', 'C', 'D', 'E', 'F'};
std::list<std::string> c = {"α", "β", "γ", "δ", "ε"};
for (const auto& [x, y, z] : std::views::zip(a, b, c))
    std::cout << x << ' ' << y << ' ' << z << '\n';

输出如下:

1
2
3
4
1 A α
2 B β
3 C γ
4 D δ

3.5 自定义视图

本节通过使用C++范围库实现几个常用的Python函数来介绍如何自定义视图。

3.5.1 实现range

在Python中,函数range(start, stop, step)生成从startstop(不包含)、步长为step的整数序列。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> list(range(0, 10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> list(range(0, 30, 5))
[0, 5, 10, 15, 20, 25]
>>> list(range(0, 10, 3))
[0, 3, 6, 9]
>>> list(range(0, 0))
[]
>>> list(range(1, 0))
[]
>>> list(range(10, -1, -1))
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>> list(range(12, 2, -3))
[12, 9, 6, 3]

C++中的views::iota视图具有类似的功能。与Python的range()一样,元素都是在迭代时生成的。但是views::iota不支持指定步长。另外,当上界小于下界时会导致无限循环,而不是生成空序列。

为了解决这两个问题,可以使用views::take_whileviews::stride(C++23引入)。另外,可以使用auto声明来避免编写复杂的返回类型。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <ranges>
#include <stdexcept>

auto range(int start, int stop, int step = 1) {
    if (step == 0)
        throw std::invalid_argument("step must not be zero");
    return std::views::iota(start)
        | std::views::take_while([stop](int i) { return i < stop; })
        | std::views::stride(step);
}

void print(auto&& r) {
    std::cout << "[ ";
    for (const auto& e : r)
        std::cout << e << ' ';
    std::cout << "]\n";
}

int main() {
    print(range(0, 10));
    print(range(1, 11));
    print(range(0, 30, 5));
    print(range(0, 10, 3));
    print(range(0, 0));
    print(range(1, 0));
    return 0;
}

输出如下(https://godbolt.org/z/YEvnqToab):

1
2
3
4
5
6
[ 0 1 2 3 4 5 6 7 8 9 ]
[ 1 2 3 4 5 6 7 8 9 10 ]
[ 0 5 10 15 20 25 ]
[ 0 3 6 9 ]
[ ]
[ ]

现在已经实现了正数步长。为了实现负数步长,首先尝试使用if语句增加一个分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
auto range(int start, int stop, int step = 1) {
    if (step == 0)
        throw std::invalid_argument("step must not be zero");
    if (step > 0) {
        return std::views::iota(start)
            | std::views::take_while([stop](int i) { return i < stop; })
            | std::views::stride(step);
    }
    else {
        return std::views::iota(stop + 1)
            | std::views::take_while([start](int i) { return i <= start; })
            | std::views::reverse
            | std::views::stride(-step);
    }
}

然而,这会导致编译失败,得到一堆晦涩难懂的错误消息(https://godbolt.org/z/TPcME3bGf):

1
error: inconsistent deduction for auto return type: 'std::ranges::stride_view<std::ranges::take_while_view<std::ranges::iota_view<int, std::unreachable_sentinel_t>, range(int, int, int)::<lambda(int)> > >' and then 'std::ranges::stride_view<std::ranges::reverse_view<std::ranges::take_while_view<std::ranges::iota_view<int, std::unreachable_sentinel_t>, range(int, int, int)::<lambda(int)> > > >'

简单来说就是if语句两个分支的返回类型不同(虽然都是ranges::stride_view,但模板参数不同,因此编译器认为是不同的类型)。一个函数具有多种返回类型在C++中是不合法的。

为了解决这一困难,考虑换一种思路——使用等差数列通项公式:

\[r_i = start + i * step \ (0 \le i < length)\] \[length = \left \lfloor \frac{\vert stop - start \vert - 1}{\vert step \vert} \right \rfloor + 1\]

这样可以将步长为正数和负数的两种情况统一起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int range_length(int start, int stop, int step) {
    int lo, hi;

    if (step > 0) {
        lo = start;
        hi = stop;
    }
    else {
        lo = stop;
        hi = start;
        step = -step;
    }

    if (lo >= hi)
        return 0;
    return (hi - lo - 1) / step + 1;
}

auto range(int start, int stop, int step = 1) {
    if (step == 0)
        throw std::invalid_argument("step must not be zero");
    int length = range_length(start, stop, step);
    return std::views::iota(0, length)
        | std::views::transform([start, step](int i) { return start + i * step; });
}

现在可以使用负数步长了(https://godbolt.org/z/P1cbE8vdq):

1
2
print(range(10, -1, -1));
print(range(12, 2, -3));
1
2
[ 10 9 8 7 6 5 4 3 2 1 0 ]
[ 12 9 6 3 ]

注:这里的实现参考了CPython的rangeobject.c

3.5.2 视图概念

自定义视图可能比较复杂,难以通过组合标准视图得到。在这种情况下,就需要实现一个视图类。

要使一个类被认为是视图,必须满足概念ranges::view,其定义如下:

1
2
3
4
5
template<class T>
concept view = ranges::range<T> && std::movable<T> && ranges::enable_view<T>;

template<class T>
constexpr bool enable_view = std::derived_from<T, view_base> || std::derived_from<T, ranges::view_interface<T>>;

也就是说,“视图”就是可移动且派生自ranges::view_interface的范围。另外,std::string_viewstd::span也被认为是视图。

视图类必须提供成员函数begin()end(),分别返回开始和结束迭代器。ranges::view_interface类会根据迭代器类别提供其他成员函数,如size()empty()operator[]等。迭代器通常实现为视图类的内部类,并在operator++operator*中实现核心计算逻辑。

下面是用视图类实现的range()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <cmath>
#include <iostream>
#include <iterator>
#include <ranges>
#include <stdexcept>

class range : public std::ranges::view_interface<range> {
public:
    class iterator {
    public:
        using iterator_category = std::forward_iterator_tag;
        using difference_type = ptrdiff_t;
        using value_type = int;
        using pointer = const int*;
        using reference = const int&;

        iterator() = default;
        iterator(int value, int step) :value_(value), step_(step) {}

        int operator*() const { return value_; }
        iterator& operator++() { value_ += step_; return *this; }
        iterator operator++(int) { iterator tmp = *this; ++*this; return tmp; }

        bool operator==(const iterator& other) const { return value_ == other.value_; }
        bool operator!=(const iterator& other) const { return !(*this == other); }

    private:
        int value_;
        int step_;
    };

    range(int start, int stop, int step = 1) :start_(start), step_(step) {
        if (step == 0)
            throw std::invalid_argument("step must not be zero");
        stop_ = (step > 0 && start >= stop || step < 0 && start <= stop) ? start
                : start + step * ceil((stop - start) / static_cast<double>(step));
    }

    explicit range(int stop) :range(0, stop, 1) {}

    iterator begin() const { return iterator(start_, step_); }
    iterator end() const { return iterator(stop_, step_); }

private:
    int start_, stop_, step_;
};

void print(auto&& r) {
    std::cout << "[ ";
    for (const auto& e : r)
        std::cout << e << ' ';
    std::cout << "]\n";
}

int main() {
    static_assert(std::forward_iterator<range::iterator>);
    static_assert(std::ranges::forward_range<range>);
    static_assert(std::ranges::view<range>);

    print(range(10));
    print(range(1, 11));
    print(range(0, 30, 5));
    print(range(0, 10, 3));
    print(range(0, 0));
    print(range(1, 0));
    print(range(10, -1, -1));
    print(range(12, 2, -3));
    print(range(1000000, 2000000) | std::views::take(5));
    return 0;
}

程序输出如下(https://godbolt.org/z/z3o3njerM):

1
2
3
4
5
6
7
8
9
[ 0 1 2 3 4 5 6 7 8 9 ]
[ 1 2 3 4 5 6 7 8 9 10 ]
[ 0 5 10 15 20 25 ]
[ 0 3 6 9 ]
[ ]
[ ]
[ 10 9 8 7 6 5 4 3 2 1 0 ]
[ 12 9 6 3 ]
[ 1000000 1000001 1000002 1000003 1000004 ]

从最后一个打印语句可以看到,自定义视图range可以和标准库视图一样通过管道运算符组合。

3.5.3 实现itertools.accumulate

C++20的范围库并没有提供数值算法(如std::accumulate()),下面用视图类实现Python的itertools.accumulate()函数。

与C++的accumulate()算法不同,Python的accumulate()函数生成一个累加序列,而不是只返回最终结果。另外,还可以提供一个函数以自定义“累加”操作。例如:

1
2
3
4
5
6
7
8
9
>>> from itertools import accumulate
>>> import operator
>>> data = [3, 4, 6, 2, 1, 9, 0, 7, 5, 8]
>>> list(accumulate(data))  # prefix sum
[3, 7, 13, 15, 16, 25, 25, 32, 37, 45]
>>> list(accumulate(data, operator.mul))  # prefix product
[3, 12, 72, 144, 144, 1296, 0, 0, 0, 0]
>>> list(accumulate(data, max))  # prefix max
[3, 4, 6, 6, 6, 9, 9, 9, 9, 9]

下面是用视图类实现的accumulate()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <iostream>
#include <iterator>
#include <functional>
#include <ranges>
#include <vector>

template<class Range, class BinaryOperation = std::plus<>, class T = typename Range::value_type>
class accumulate : public std::ranges::view_interface<accumulate<Range, BinaryOperation, T>> {
public:
    class iterator {
    public:
        using iterator_category = std::forward_iterator_tag;
        using difference_type = ptrdiff_t;
        using value_type = T;
        using pointer = const T*;
        using reference = const T&;

        iterator() = default;
        iterator(std::ranges::iterator_t<Range> current, accumulate* parent)
                :current_(current), parent_(parent), accumulated_(parent->init_) {
            acc();
        }

        reference operator*() const { return accumulated_; }

        iterator& operator++() {
            ++current_;
            acc();
            return *this;
        }

        iterator operator++(int) {
            iterator tmp = *this;
            ++*this;
            return tmp;
        }

        bool operator==(const iterator& other) const { return current_ == other.current_; }
        bool operator!=(const iterator& other) const { return !(*this == other); }

    private:
        void acc() {
            if (current_ != std::ranges::end(parent_->base_))
                accumulated_ = std::invoke(parent_->op_, accumulated_, *current_);
        }

        std::ranges::iterator_t<Range> current_;
        accumulate* parent_;
        T accumulated_;
    };

    explicit accumulate(Range base, BinaryOperation op = {}, T init = {})
        :base_(std::move(base)), op_(std::move(op)), init_(std::move(init)) {}

    iterator begin() { return iterator(std::ranges::begin(base_), this); }
    iterator end() { return iterator(std::ranges::end(base_), this); }

private:
    Range base_;
    BinaryOperation op_;
    T init_;
};

void print(auto&& r) {
    std::cout << "[ ";
    for (const auto& e : r)
        std::cout << e << ' ';
    std::cout << "]\n";
}

int main() {
    static_assert(std::forward_iterator<accumulate<std::vector<int>>::iterator>);
    static_assert(std::ranges::forward_range<accumulate<std::vector<int>>>);
    static_assert(std::ranges::view<accumulate<std::vector<int>>>);

    std::vector<int> data = {3, 4, 6, 2, 1, 9, 0, 7, 5, 8};
    print(accumulate(data));
    print(accumulate(data, std::multiplies<>(), 1));
    auto max = [](int a, int b) { return a > b ? a : b; };
    print(accumulate(data, max, -1));
    return 0;
}

程序输出如下(https://godbolt.org/z/znjc5Kr6r):

1
2
3
[ 3 7 13 15 16 25 25 32 37 45 ]
[ 3 12 72 144 144 1296 0 0 0 0 ]
[ 3 4 6 6 6 9 9 9 9 9 ]

可以看出,用C++实现自定义视图类时,不得不编写大量的样板代码(如迭代器的5个成员类型、后缀++==!=等)。作为对比,下面是用Python实现的accumulate()。利用yield语句,代码要简短得多。

1
2
3
4
5
6
def accumulate(iterable, function, initial):
    total = initial
    yield total
    for element in iterable:
        total = function(total, element)
        yield total

参考

This post is licensed under CC BY 4.0 by the author.