打开导航 打开导航 Open menu
量化交易系统开发(C++)/ 期权策略与风控 / 行业研究分析
量化交易系统開發(C++)/ 期权策略与风控 / 行业研究分析
C++ trading systems / options strategy & risk controls / equity research
adrian@adrianxv.cn

C++20: Ranges、view 和 concepts C++20: Ranges、view 和 concepts C++20: Ranges、view 和 concepts

这是学习 A Tour of C++ by Bjarne Stroustrup Chapter 14 Ranges 的读书笔记。

这里有四个概念

  • ranges
  • views
  • range
  • view

它们四个之间的关系是:

<ranges>                     头文件
└── std::ranges              命名空间
    ├── range                range 概念
    ├── view                 view 概念
    ├── filter_view          具体类型
    └── views                创建 view 的工具集合
        ├── filter
        ├── take
        └── transform
  • ranges 是库/命名空间,views 是其中用于创建 view 的工具集合
  • range 表示 “任何能按顺序遍历元素的对象或表达式”
  • view 是一种 range,特点是不复制原数据,只是作为一种视图
  • filter_view、take_view 这类具体的 view 类型由 views:: 的一些操作得到(下面讲)

ranges 命名空间提供算法

这种遍历是旧式,有两个迭代器:

sort(v.begin(), v.end());
  • 旧式写法不会自动越界
  • 但是有可能出现 sort(v1.begin(), v2.end()) 这种起点和终点分属两个容器的情况

新式写法:

ranges::sort(v);
  • 这样确保起点和终点属于一个序列

view 是对已有 range 的一种非拥有式观察。

view 描述了对于容器的访问规则。有了 view,在对容器元素进行筛选、转换、取前几个、跳过前几个或反向查看时,无需创建中间容器。

range

vector、list、built-in array、std::string 都是 range。

但是 C 风格字符串不是 range,因为 C 风格字符串只是一个指针。

一种针对 range 的分类方式是看它是否是 view。除了可以按照是否为 view 分类外,还可以按照遍历能力分成:

  • input range
  • forward range
  • bidirectional range
  • random access range

一个对象可以同时处在两套分类中,比如:

std::vector<int> v;

它是一个 range,不是 view,是 forward range,也是 bidirectional range 和 random access range。

view

  • 很像数据库的视图,也有点像“带筛选规则的引用”
  • view 通常只保存“从哪里获取数据”和“按什么规则取”
  • 底层 range 销毁后,view 仍然存在,但它指向的数据已经不存在了。之后遍历 view,就像悬空引用一样,是未定义行为
  • 由于 view 只是“带筛选规则的引用”,因此 view 的复制很便宜,通常可以按值传递
  • 没有一种叫“view”的类型,只有更具体的 filter_view、take_view 等,由 std::ranges::views 的各种操作得到,叙述如下:

std::ranges::views

  • 是属于 Ranges 库的内容,完整写法是 std::ranges::views
  • 以下的 views:: 操作,返回类型是 std::ranges::*_view,比如 std::ranges::filter_viewstd::ranges::take_view

std::ranges::views::filter

专门用来做筛选的 view。假设有:

vector<int> numbers {1, 2, 3, 4, 5};

然后想从中筛选出偶数,传统写法是写一个判断:

for (int x: numbers) {
    if (x % 2 == 0)
        cout << x << ' ';
}

但是有了 views::filter 之后:

auto even_numbers =
    numbers | views::filter([](int x) {
        return x % 2 == 0;
    });

在 ranges 的代码中,| 这个符号表示把左边的 range 交给右边的 view 来操作。

  • 这里的 even_numbers 会被推断为一个 filter_view 类型
  • 这里 views::filter 需要一个“筛选规则”的 predicate,而 lambda 很适合临时写这种规则

std::ranges::views::take

用来限制一个 range 的元素,让你只看到 range 的前几个元素。假设有:

vector<int> v{10, 20, 30, 40, 50};

如果想获取前三个元素:

int count = 0;
for (int x: v) {
    if (count >= 3) {
        break;
    }
    cout << x << ' ';
    count++;
}

而有了 views::take 之后:

for (int x: v | views::take(3)) {
    cout << x << ' ';
}

generator

generator 是为了“按需产生一个 range”,不需要先有一个 vector 装着元素再用来遍历。

  • empty_view:产生 0 个元素
  • single_view {x}:产生 1 个元素,也就是 x
  • iota_view {x, y}:按递增规则产生多个元素,产生 x 到 y - 1
  • istream_view:从输入流逐个读取元素

views::empty

auto nothing = views::empty<int>;  // 根本没有元素的 range
for (int x: nothing) {
    cout << x; // 一次也不会执行
}

目的是有时候某分支没有结果,仍可返回一个合法 range。

views::single

auto one = views::single(10);

适用于算法需要接收 range,但是当前只有一个值的情况。

views::iota

auto numbers = views::iota(3, 7);

产生 3、4、5、6。其实就类似 Python 的 range(3, 7)

只给一个参数时也可产生无限序列,通常接 views::take()

auto numbers = views::iota(3) | views::take(4);

views::istream

auto numbers = views::istream<int>(std::cin);

输入什么数字,就用什么数字生成 range。

pipelining

指的就是前面的 | 操作,把多个 view 操作按顺序连起来。比如:

numbers | views::filter(is_even()) | views::take(3);

其实这样就很像机器学习里面的数据清洗流程,但是在 C++ 当中这都是通过 views 来完成的,因此没有复制原数据。

concepts

concepts 就是用来帮助编译器判断当前这个对象是否适宜做某些操作的。可以类比数学中的集合概念:预先定义好了一些条件,满足这个条件的所有类型构成一个集合,这个集合就是 concept。编译器在编译时要检查的就是:当前这个类型是否属于这个集合;若不属于,则存在非法操作。因此应当把 concept 看作“类型谓词”,而不是对象。

通常来说,我们不需要自定义 concept,因为标准库已经实现了大部分我们会用到的 concepts 了。如果一定要自定义 concept,则使用 concept 关键词,语法类似:

template<class T>
concept HasSize = requires(T x) {
    x.size();
};

有三类 concepts:

  • 类型 concepts
    • 比如是不是整数,能否复制
  • 迭代器 concepts
    • 能不能向前走,能不能向后走
  • range concepts
    • 是不是 forward_range

用 concept 约束自定义算法

template<forward_range R>
requires sortable<iterator_t<R>>
void my_sort(R& r) {
    // ...
}

这里有两个要求:

  • R 必须是 forward_range
  • R 的迭代器必须 sortable

这里使用的都是标准库定义好的 concept,没有自定义 concept,因此直接用就可以了。

用 concepts 区分 range 的能力

range
sized_range
input_range
forward_range
bidirectional_range
random_access_range
contiguous_range

这些 concepts 让库可以根据 range 的能力选择或限制算法实现。这里其实就是本笔记一开始说到的“根据遍历能力划分 range”。

用 concepts 描述 iterator 和操作对象

input_iterator    // 描述迭代器能做什么
forward_iterator  // 描述迭代器能做什么
sortable          // 描述元素是否能排序
predicate         // 描述函数能否充当谓词
sentinel_for      // 描述一个结束标记是否能和迭代器配合