跳转到内容

C++/ranges

维基教科书,自由的教学读本
< C++

<ranges>是C++20引入的标准库。<ranges>中定义了std::ranges和std::views命名空间。

范围”(range)是一个可以迭代的对象的集合,支持begin()和end()迭代器的结构都是范围。这包括大多数STL容器。

视图”(view)从底层的范围返回数据,但不拥有任何数据,并通过某种算法或操作转换底层的范围。视图是惰性的,只在范围迭代(即请求元素)时才会惰性求值,而不是在创建视图时执行。view是容器的视图,具有一定的数据操作能力,而且还重载了"|",能够实现多个view的串联操作。

视图适配器是一个对象,可接受一个范围,并返回一个视图对象。视图适配器可以使用|操作符连接到其他视图适配器。视图适配器从|操作符的左侧获得范围操作数。|操作符会从左到右求值。视图适配器位于std::views命名空间中。

一个简单例子:

#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<int>vi{0,1,2,3,4,5,6,7,8,9};
    auto tview =vi|std::views::reverse|std::views::take(5);    //视图适配器
    for(int i:tview)
        std::cout<<i<<" ";
    //输出为:9 8 7 6 5 

    // 不会产生数据
    auto view = std::views::iota(0, 10); 
    // 开始产生数据
    for (auto i : view)
    {
       std::cout << i << std::endl;
    }
    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)视图使用了一个转换函数,对每个元素应用转换函数后,返回底层序列的视图。例如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 参数的所有元素的视图
  • 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个元素

ranges库还包括一些factory函数:

  • 对象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是容器上的一个抽象概念,可以理解成指明首末位置的迭代器,即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