【C++20】ranges标准库

ranges标准库

ranges是对一系列数据的抽象,只有有头有尾,它既可以是一个有界容器,也可以是一个无穷列表.一旦有了这种抽象,就可以进一步构造range的适配器,它能够被管道操作符进行组合,以便对数据进行灵活的处理,而这些处理都是延迟计算的

用ranges编程生成小于1000的奇平方数:

auto res = std::views::iota(1) //生成从1开始的无穷序列
            | std::views::transform([](auto n){return n * n;})
            | std::views::filter([](auto n){return n % 2 == 1;})
            | std::views::take_while([](auto n){return n < 1000;});
for(auto x : res){
    std::cout << x << std::endl;
}

通过iota创建一个从1开始的无穷数列ranges,随后与各个适配器进行组合处理,transform对ranges中的每个数进行求平方操作,对平方的结果通过filter进行过滤,过滤条件为奇数,最终的奇数交给take_while处理,取所有小于1000的奇数

这种函数式编程风格有如下特点

  • 延迟计算
  • 灵活的组合能力
  • 使程序通过消除循环的方式避免大量状态操作

range访问操作

C++标准库中的容器都可以视作一个range,普通数组和自定义类型也可以对range进行建模
标准库中提供了非成员函数去访问这些range

  • std::ranges::begin

考虑对象r为一个range,函数调用std::ranges::begin®返回一个起始迭代器

  1. 若r为右值临时对象,则编译错误
  2. 若r为数组,则返回结果为r+0
  3. 若存在成员函数begin且满足要求则返回r.begin()
  4. 若r通过依赖查找(ADL)找到begin( r ),且返回类型满足要求,则结果为begin( r )

考虑如下代码

namespace ns{
    struct Foo{};
    void swap (Foo &a , Foo & b)noexcept {puts("Foo swap");}
};

ns::Foo a , b;
std::swap(a , b);

上述代码的调用结果总是使用标准库中更通用的std::swap版本,而不是自定义的交换函数,为避免这个问题,在泛型代码中,常常使用如下两步模式

using std::swap;  //第一步,引入std::swap
swap(a , b);    //第二步,未限定名称查找(ADL)

第一步,在当前作用域范围内引入std::swap;第二步,未限定名称查找。编译器会优先去对象所在名称空间下查找

而使用新一代ranges标准库里提供的算法,即便使用ADL也能实现上述效果

ranges::swap(a , b); //Foo swap

ranges标准库通过这种方式可以实现与传统标准算法库的精准隔离,即便使用两步模式编译器也会优先使用函数对象进行调用

using std::ranges::swap;
swap(a , b);
  • std::ranges::end 用来返回ranges的终止迭代器

终止迭代器也叫哨兵迭代器

std::vector<int> v{3, 1, 4};
if (std::ranges::find(v, 5) != std::ranges::end(v))
    std::cout << "在 vector v 中找到了 5!\n";
  • std::ranges::size 用来返回一个range的大小
  1. 若为数组则返回数组的大小
  2. 否则,使用成员函数r.size()
  3. 否者,通过ADL的非泛型自由函数size( r )
  4. 否则,使用std::ranges::end( r )-std::ranges::begin( r )
auto il = {7};     // std::initializer_list
std::cout << "ranges::size(il) == " << std::ranges::size(il) << '\n';

int array[]{4, 5}; // array has a known bound
std::cout << "ranges::size(array) == " << std::ranges::size(array) << '\n';
  • std::ranges::empty 判断一个range是否为空
  1. 使用bool(r.empty())
  2. 否则,使用std::ranges::size( r )是否为0
  3. 否则,通过std::ranges::begin( r )==std::ranges::end( r )判断
template <std::ranges::input_range R>
void print(char id, R&& r)
{
    if (std::ranges::empty(r))
    {
        std::cout << '\t' << id << ") Empty\n";
        return;
    }
 
    std::cout << '\t' << id << ") Elements:";
    for (const auto& element : r)
        std::cout << ' ' << element;
 
    std::cout << '\n';
}
  • std::ranges::data 获得一个rang的数据地址
  1. 若r为临时右值对象则编译错误
  2. 否则,使用成员函数r.data(),切返回为指针类型
  3. 否则,使用r.begin(),要求返回的迭代器对连续迭代器建模
#include <cstring>
#include <iostream>
#include <ranges>
#include <string>
 
int main()
{
    std::string s{"Hello world!\n"};
 
    char a[20]; // storage for a C-style string
    std::strcpy(a, std::ranges::data(s));
    // [data(s), data(s) + size(s)] is guaranteed to be an NTBS
 
    std::cout << a;
}

ranges相关概念

  • std::ranges::range

range概念定义了允许迭代其元素的类型,它要求一个可迭代的类型能够返回迭代元素的起始于哨兵迭代器,数组,标准库中的容器都可以视作range

template< class T >
concept range = requires( T& t ) {
  ranges::begin(t); // equality-preserving for forward iterators
  ranges::end  (t);
};
  • std::ranges::borrowed_range

引用要求引用的生命周期小于它所引用对象的生命周期,这样避免了悬挂引用
borrowed_range基于此概念,表达对range的借用(这里使用左值引用而不是右值,因为右值常常用来表达移动语义,会转移对象所有权)

template<std::ranges::borrowed_range R>
void f1(R && r){
    std::cout << "concept borrowed_range" << std::endl;
    return ;
}

f(vector<int>{1,2,3,4});//编译错误,一个右值
vector<int>vec{1,2,3,4};
f(vec);

string_view是C++17标准库引入的字符串类,它拥有和const char *一样小的传递开销

f(vector<int>{1,2,3,4});//编译错误,一个右值
f(string_view{"1234"});//编译成功,一个右值

这里编译成功的原因是string_view对变量模板enable_borrowed_range进行特化,使得它满足其概念要求

  • std::ranges::sized_range

sized_range 概念指定在常数时间内以 size 函数知晓其大小的 range 类型

  • std::ranges::view

它在range概念的基础上,要求能够在常量时间复杂度内进行移动构造,移动赋值与析构

enable_view 变量模板用于指示 range 是否为 view

根据定义,标准库中的容器都只是range而不是view但string_view是view

  • 其他概念

在这里插入图片描述

rang 实用组件

  • std::ranges::view_interface

用来创建自己的view

#include <ranges>
#include <vector>
#include <iostream>
 
template <class T, class A>
class VectorView : public std::ranges::view_interface<VectorView<T, A>> {
public:
    VectorView() = default;
    VectorView(const std::vector<T, A>& vec) :m_begin(vec.cbegin()), m_end(vec.cend()) {}
    auto begin() const { return m_begin; }
    auto end() const { return m_end; }
private:
    typename std::vector<T, A>::const_iterator m_begin{}, m_end{};
};
 
int main()
{
    std::vector<int> v = {1, 4, 9, 16};
 
    VectorView view_over_v{v};
 
    // 我们能以 begin() 与 end() 迭代。
    for (int n : view_over_v) {
        std::cout << n << ' ';
    }
    std::cout << '\n';
 
    // 我们能在继承 view_interface 时免费地获得 operator[]
    // 因为我们满足 random_access_range 概念。
    for (std::ptrdiff_t i = 0; i < view_over_v.size(); i++) {
        std::cout << "v[" << i << "] = " << view_over_v[i] << '\n';
    }
}
// 1 4 9 16 
// v[0] = 1
// v[1] = 4
// v[2] = 9
// v[3] = 16

range 工厂

  • std::ranges::views::empty

无元素的空 view

#include <ranges>
 
int main()
{
    std::ranges::empty_view<long> e;
    static_assert(std::ranges::empty(e));
    static_assert(0 == e.size());
    static_assert(nullptr == e.data());
    static_assert(nullptr == e.begin());
    static_assert(nullptr == e.end());
}
  • std::ranges::views::single

含有具有指定值的单个元素的 view

  • std::ranges::views::iota

由通过重复对某个初值自增所生成的序列组成的 view

for (int i : std::ranges::iota_view{1, 10})
    std::cout << i << ' ';
std::cout << '\n';
//1 2 3 4 5 6 7 8 9
  • std::ranges::istream_view

由在关联的输入流上相继应用 operator>> 获得的元素组成的 view

auto v = std::ranges::istream_view<int>(std::cin);
for(auto e : v)std::cout<< e << " ";

range 适配器

  • std::ranges::views::all

尝试把输入的range转化成view

std::vector<int> v{0, 1, 2, 3, 4, 5};
for (int n : std::views::all(v) | std::views::take(2))//转化并取前两个
    std::cout << n << ' ';
  • std::ranges::views::filter

filter接受一个rabge和一个谓词,并返回满足谓词的filter_view

#include <iostream>
#include <ranges>
 
int main()
{
    auto even = [](int i){ return 0 == i % 2; };
    auto square = [](int i){ return i * i; };
 
    for (int i : std::views::iota(0, 6) 
               | std::views::filter(even) 
               | std::views::transform(square))
        std::cout << i << ' ';
}
//0 4 16
  • std::ranges::views::transform

transform适配器接受一个rang和一个计算函数,返回函数计算后的transform_view

  • std::ranges::views::take 接受一个range和一个N,返回range的前N个元素
  • std::ranges::views::take_while 接受一个range和一个谓词,它取所有元素直到不满足谓词
  • std::ranges::views::drop 接受一个range和一个N,扔掉前N个元素并返回剩余元素
  • std::ranges::views::drop_while 接受一个range和一个谓词,它扔掉所有元素直到不满足谓词
  • std::ranges::views::join 表示由从拉平范围的视图获得的序列组成的 view。
  • std::ranges::views::split split_view 接收一个 view 与一个分隔符,并按分隔符切割 view 为子范围。
  • std::ranges::views::common 返回传统迭代器
auto r1 = std::ranges::subrange{i1, std::default_sentinel};
//  auto e1 = std::accumulate(r1.begin(), r1.end(), 0); // 错误:要求“共同范围”
auto c1 = std::ranges::common_view{r1};
std::cout << "accumulate: " << std::accumulate(c1.begin(), c1.end(), 0) << '\n';
  • std::ranges::views::reverse 代表有逆序的底层 view 的视图。

改善

ranges标准库改变了algorithm标准库

在这里插入图片描述


  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值