C++20视图(ranges)探究

范围库提供了一种处理容器的新方式,通过视图实现对数据的惰性操作,避免了复制和空间浪费。视图是转换基础范围的对象,如take_view和filter(),它们不存储数据,仅在迭代时操作。C++20的范围库还包括工厂函数如iota,以及在std::ranges命名空间内的算法,如sort,可以直接作用于范围,增强了代码的简洁性和效率。

出现的原因

我们在容器进行数据的处理时,不免的会修改容器本身,此时我们要常常创建一个原容器一摸一样的拷贝来维护原先的状态,这样不仅浪费了空间,并且会引起代码的不直观。因此范围库的引入为过滤和处理容器提供了一种新范例。

下方解释了几个术语:

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

  1. "视图"是转换另一个基础范围的范围。视图是惰性的,只在范围迭代时操作。视图从底层范围返回数据,不拥有任何数据。视图的运行时间复杂度是O(1)。

  1. 视图适配器是一个对象,可接受一个范围,并返回一个视图对象。视图适配器可以使用|操作符连接到其他视图适配器。

几个简单例子

范围和视图类在<ranges>头文件中,<ranges>中定义了std::ranges和std::ranges::views命名空间。

  1. 将视图应用于范围

const vector<int> nums{1,2,3,4,5,6,7,8,9,10};
auto result=std::ranges::take_view(nums,5);
for(auto it:result)cout<<v<<" ";

输出为:

1 2 3 4 5

std::ranges::take_view(range,n):返回前n个元素的视图

视图适配器版本的take_view():

auto result=nums|views::take(5);
for(auto it:result)cout<<it<<" ";

输出为:

1 2 3 4 5

视图适配器位于std::ranges::views命名空间中。视图适配器从|操作符的左侧获得范围操作数。|操作符会从左到右求值。

  1. 因为视图适配器可迭代,所以也有资格作为范围。这使得他们可以连续使用:

const vector<int> nums{1,2,3,4,5,6,7,8,9,10};
auto result =nums|views::take(5)::views::reverse;

输出为:

5 4 3 2 1

  1. filter()视图使用一个谓词函数:

auto result =nums|views::filter([](int i){return 0==i%2;});

输出为:

2 4 6 8 10

  1. transform()视图使用了一个转换函数:

auto result =nums|views::transform([](int&i){return i*=i;});

输出为:

1 4 9 16 25 36 49 64 81 100

  1. 视图和适配器可以使用于任何范围:

const vector<string> words{"one","two","three","four","five"};
auto result=words|views::reverse;

输出为:

five four three two one

  1. 范围库还包括一些factory函数。itoa将生成一系列递增的值:

auto rnums =view::iota(1,10);

输出为:

1 2 3 4 5 6 7 8 9

视图的工作原理

为了满足范围的基本要求,对象必须至少有两个迭代器begin()和end()。大部分STL都范围的要求,包括vector,string,array,map等。不过容器适配器除外,如stack和queue等,因为他们没有起始迭代器和结束迭代器。

视图是一个对象,操作一个范围并且返回一个修改后的范围。视图为惰性操作的,不包含自己的数据。不保留底层数据的副本,只是根据需要返回底层元素的迭代器。

vector<int>vi{0,1,2,3,4,5};
std::ranges::take_view tv{vi,2};
for(int i:tv)cout<<i<<" ";
cout<<endl;

输出为:

0 1

本例中,take_view对象接受两个参数,一个范围和一个计数,结果是一个包含vector中第一个count对象的视图。在for循环中的迭代过程中,take_view对象会根据对象需要返回指向vector对象的元素的迭代器。

在此过程中不修改vector对象。

范围命名空间中的许多视图在视图命名中间中都有相应的范围适配器,这些适配器可以使用按位或(|)作为管道。

vector<int> vi{0,1,2,3,4,5,6,7,8,9};
auto tview =vi|views::take(2);
for(int i:tview)cout<<i<<" ";
cout<<endl;

输出为:

0 1

|操作符从左到右求值。因为范围是适配器的结果是另一个范围,所以这些适配器表达式可以连接起来。

vector<int>vi{0,1,2,3,4,5,6,7,8,9};
auto tview =vi|views::reverse|views::take(5);
for(int i:tview)cout<<i<<" ";
cout<<endl;

输出为:

9 8 7 6 5

标准库包括一个过滤视图,用于定义简单的过滤器:

vector<int>vi{0,1,2,3,4,5,6,7,8,9};
auto even =[](int i){return 0==i%2;};
auto tview =vi|views::filter(even);
for(int it:tview)cout<<it<<" ";
cout<<endl;

输出为:

0 2 4 6 8

库中有相当多的有用的视图和视图适配器。可访问(https://j.bw.org/ranges)获取完整的列表。

还有一些内容

从C++20开始<algorithm>头文件中的大多数算法都会基于范围。这些版本在<algorithm>头文件中,但是在std::ranges命名空间中,这将他们与传统算法分离开。所以无需再调用两个迭代器的算法

sort(v.begin(),v.end());

现在可以用范围来调用

std::ranges::sort(v);

这当然更方便,但它真正的意义在哪里呢?

当对vector一部分排序时的情况。可以用老方法做:

sort(v.begin()+5,v.end());

这将对vector的前5个元素进行排序。范围版本中,可以使用视图来跳过前5个元素。

std::ranges::sort(std::ranges::views::drop(v,5));

甚至可以组合视图:

std::ranges::sort(std::ranges::views::drop(std::ranges::views::reverse(v),5));

也可以使用范围迭代器作为std::ranges::sort的参数:

std::ranges::sort(v|std::ranges::view::reverse|std::ranges::views::drop(5));

用传统的排序算法和vector迭代器完成:

std::ranges::sort(v.begin()+5,v.end());

可以在cppreference(https://j.bw.org/algoranges)上找到限制使用范围的完整算法列表.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Reol520

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值