C++/ranges
<ranges>是C++20引入的标准库。<ranges>中定义了std::ranges和std::views命名空间。
基本概念
[编辑]“范围”(range)定义为:任何能够提供迭代器对 [begin, end) 的对象。即:能被begin/end遍历的一段东西。这包括标准容器:vector, list, array等;原生数组:int arr[10];特殊的范围range:istream_view (从流读取),iota_view (生成序列)。
// Range 概念的基本要求
template<typename T>
concept Range = requires(T& t) {
std::ranges::begin(t); // 必须有 begin()
std::ranges::end(t); // 必须有 end()
};
#include <ranges>
#include <vector>
#include <string>
#include <array>
// 验证各种类型是否满足 Range 概念
static_assert(std::ranges::range<std::vector<int>>); // true
static_assert(std::ranges::range<std::string>); // true
static_assert(std::ranges::range<std::array<int, 5>>); // true
static_assert(std::ranges::range<int[10]>); // true
“视图”(view):轻量级的范围range的包装器(零拷贝),是轻量级、非拥有的range,支持常数时间复制、移动和赋值。惰性组合、惰性求值(操作延迟到迭代时)、时间复杂度O(1)构造/析构、可组合性(通过 | 管道符连接)、通常不拥有数据(或特例拥有少量数据),仅引用底层范围range。从底层的范围range返回数据,但不拥有任何数据。本质上,迭代时view返回下一个符合过滤条件的元素。
// View 概念的要求
template<typename T>
concept View = std::ranges::range<T> &&
std::movable<T> &&
std::ranges::view_base<T>;
#include <ranges>
#include <vector>
// 验证各种视图类型
static_assert(std::ranges::view<std::ranges::iota_view<int, int>>); // true
static_assert(std::ranges::view<decltype(std::views::iota(0) | std::views::take(5))>); // true
// 容器不是 View(因为复制成本高)
static_assert(!std::ranges::view<std::vector<int>>); // false
范围工厂(Range factory):从“无现成输入range”开始,制造一个 range/view。包括:
- 对象std::views::empty, void -> view, 使用std::views::empty<T>即可直接获得对象
- 对象std::views::single, any -> view, 单对象的std::ranges::view
- 对象std::views::iota, iterator | (iterator, sentinel) -> view, 一般哨位边界的有限或无限递增序列 例如,itoa将生成一系列递增的值,如
auto rnums =std::views::iota(1,10); - 对象std::views::counted, (iterator, count) -> view, 计数哨位边界的有限递增序列
- 对象std::views::istream<T>, istream<T> -> view, 输入流转std::ranges::view
- 类型std::ranges::subrange, (iterator, sentinel, [size]) | (borrowed_range, [size]) -> subvrange, 迭代器-哨位对
- 类型std::ranges::ref_view, range -> viewable_range 借用
- 类型std::ranges::owning_view, range -> viewable_range 占用
- 对象std::views::repeat(C++23), 由重复产生相同值的生成序列组成的视图
- 对象std::views::cartesian_product(C++23), 由n元笛卡尔积计算出的结果元组组成的视图
视图适配器(Range adaptor)是一个函数对象,属于命名空间std::views,可接受一个已有的range,返回一个range或者视图对象view。视图适配器可以使用|操作符连接到其他视图适配器。视图适配器从|操作符的左侧获得范围操作数。|操作符会从左到右求值。为什么叫 adaptor?因为它通常不拥有数据,而是对已有 range 做一层包装。包括: // 元素操作
- transform(fn) // 映射元素
- filter(pred) // 条件过滤
// 结构操作
- take(n) // 取前n元素
- drop(n) // 跳过前n元素
- reverse // 逆序
- keys/values // 键/值提取(针对pair-like)
// 生成操作
- iota(start) // 无限序列生成
- empty<T> // 空范围
一个简单例子:
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 适配器可以存储为对象
auto filter_even = std::views::filter([](int n) { return n % 2 == 0; });
auto take_three = std::views::take(3);
auto square = std::views::transform([](int n) { return n * n; });
// 组合使用
auto result = numbers | filter_even | take_three | square;
for (int n : result) {
std::cout << n << " "; // 输出: 4 16 36
}
return 0;
}
ranges库在std::views命名空间中包括一些变换函数:
- take(int)选出view里的前N个元素。如果改编后的视图包含的元素少于N,则返回所有元素。
- take_while()给定一个一元谓词 pred 和一个视图 r,它生成一个范围 [begin(r), ranges::find_if_not(r, pred)) 的视图。
- reverse()逆序双向视图里的所有元素
- filter(invokable)视图使用一个谓词函数筛选出view里的特定元素,例如
auto result = nums|std::views::filter([](int i){return 0==i%2;}); - transform(invokable)视图使用了一个转换函数,对每个元素应用转换函数后,返回底层range的视图。例如
auto result =nums|std::views::transform([](int i){return i*i;}); - drop()排除view里的前N个元素,如果改编后的视图包含少于 N 个元素,则返回一个空范围。例如:
rnums = std::views::drop(rnums,5); - drop_while()给定一个一元谓词 pred 和一个视图 r,它生成一个范围 [ranges::find_if_not(r, pred), ranges::end(r)) 的视图。
- keys()选出pair里的first成员。是elements_view<views::all_t<R>, 0> 的别名。
- values()选出pair里的second成员。是elements_view<views::all_t<R>, 1>的别名。
- all() 把一个 range 转成 view:如果输入本身就是 view,直接返回;如果是左值容器,可能返回 ref_view;如果是右值且满足条件,可能返回owning_view。
- join()将二维/多维的范围视图平展为一维视图
- split(forward_range & view)接受一个视图和一个分隔符,并将视图划分为分隔符上的子范围。分隔符可以是单个元素,也可以是元素的视图。
- common()或common_range()获取一个迭代器和哨兵具有不同类型的视图,并将其转换为具有相同类型迭代器和哨兵的相同元素的视图。对于调用期望范围的迭代器和哨点类型相同的遗留算法很有用。
- counted()计数视图显示了迭代器i和非负整数 n 的计数范围([iterator.requirements.general]) i+[0, n) 的元素的视图。
- elements()接受一个类元组值和 size_t 的视图,并生成一个值类型为已改编视图值类型的第 n 个元素的视图。
- zip(C++23) 由对已改编视图的相应元素的引用的元组组成的视图
- zip_transform(C++23) 由转换函数应用到所适应视图的相应元素的结果元组组成的视图
- adjacent(C++23) 由对已改编视图的相邻元素的引用元组组成的视图
- adjacent_transform(C++23) 由转换函数应用于所适应视图的相邻元素的结果元组组成
- join_with(C++23) 一种视图,由将范围视图平展得到的序列组成,元素之间有分隔符
- slide(C++23) 第 M 个元素是另一个视图的第 M 个到 (M + N - 1) 个元素的视图
- chunk(C++23) 一个由另一个视图的元素组成的n个大小的不重叠连续块的视图范围
- chunk_by(C++23) 将视图拆分为给定谓词返回false的每对相邻元素之间的子范围
- as_const(C++23) 将视图转换为常量范围
- as_rvalue(C++23) 将每个元素强制转换为右值的序列视图
- stride(C++23) 由另一个视图的元素组成的视图,一次向前移动N个元素
range是容器上的一个抽象概念,可以理解成指明首末位置的迭代器,即pair<begin,end>,这样range自身就包含能用于算法的足够信息,大多数算法只要用一个range参数就可以工作。基于range的概念,C++在名字空间std::ranges提供了与标准算法同名、但却使用range参数的算法,写法很简洁。从C++20开始,<algorithm>头文件中的大多数算法都会基于“范围”。这些版本在<algorithm>头文件中,但是在std::ranges命名空间中,这使它们与传统算法分离开。所以无需再调用两个迭代器的算法。例如std::sort(v.begin(),v.end());可以用范围来调用std::ranges::sort(v);使用试图适配器更为直观、易读:std::ranges::sort(v|std::ranges::view::reverse|std::ranges::views::drop(5));
range的分类
[编辑]| Concept | Description |
|---|---|
| std::ranges::input_range | can be iterated from beginning to end at least once |
| std::ranges::forward_range | can be iterated from beginning to end multiple times |
| std::ranges::bidirectional_range | iterator can also move backwards with --
|
| std::ranges::random_access_range | can jump to elements in constant-time []
|
| std::ranges::contiguous_range | elements are always stored consecutively in memory |
| std::ranges::sized_range | 范围的大小可由std::ranges::size()获得,提供size函数,能常数时间获取范围的长度;如果未提供size函数, 那么还要求
std::forward_iterator且iterator与sentinel(及iterator)可作差;如果size或者iterator的作差不能用常数时间实现, 那么可以用特化:std::ranges::disable_sized_range<T> = true且std::disable_sized_sentinel_for<iterator, iterator> = true, 强制关闭std::ranges::sized_range和std::ranges::sized_sentinel_for的特征 |
| std::ranges::common_range | 如果iterator == sentinel |
| std::ranges::viewable_range | 如果std::ranges::view 或std::ranges::borrowed_range |
| std::ranges::borrowed_range | 如果类型std::ranges::range T的值T t的t.begin()和t.end()获得的迭代器的生命周期与t无关, 那么可以认为是std::ranges::borrowed_range。由于语言层面无法自动识别生命周期的关系, 因此要特征能被识别, 还要手动特化std::ranges::enable_borrowed_range<T>为true |
对照实现C#的LINQ的功能
[编辑]std::ranges基本上可以一对一的实现C#的LINQ的功能:
| C# LINQ 方法 | C++20 std::ranges/views 组件 | 功能说明 |
|---|---|---|
| Where | std::views::filter |
过滤满足条件的元素 |
| Select | std::views::transform |
元素映射/转换 |
| Take | std::views::take |
获取前 N 个元素 |
| Skip | std::views::drop |
跳过前 N 个元素 |
| TakeWhile | std::views::take_while |
一直取元素直到条件不成立 |
| SkipWhile | std::views::drop_while |
一直跳过元素直到条件不成立 |
| OrderBy | std::ranges::sort |
排序 |
| Reverse | std::views::reverse |
反转序列 |
| Any / All / Contains | std::ranges::any_of / std::ranges::all_of |
判断元素是否存在/全部满足 |
| Zip | std::views::zip |
合并多个序列 |
| Concat | std::views::concat |
连接两个序列 |
| Range | std::views::iota |
生成连续数字序列 |
二者都是延迟执行(懒加载),不遍历就不真正计算,零拷贝、高性能。都能操作任何可枚举集合(数组、vector、string、list等等)。
例如,C#的LINQ程序:
var result = numbers
.Where(x => x % 2 == 0)
.Select(x => x * 2)
.Skip(2)
.Take(5);
对应于C++程序:
auto result = numbers
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * 2; })
| std::views::drop(2)
| std::views::take(5);