C++20 范围库:关键优势——算法的组合

2023-05-16

从概念上讲,范围(Range)是一个简单的概念:它只是一对迭代器——指向序列的开始和结束(在某些情况下是一个哨兵)。然而,这样的抽象却可以从根本上改变编写算法的方式。在这篇博文中,我将向你展示 C++20 中范围库带来的一个关键变化。

通过在迭代器上使用这一抽象层,我们可以表达更多的想法,并拥有不同的计算模型。

2023 年 3 月更新:增加了 C++23 的注释。

计算模型

让我们看一个 C++ 中“常规” STL 的简单例子。

它从一个数字列表开始,跳过第一个,选择偶数,然后以相反的顺序打印它们:

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    const std::vector numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    auto even = [](int i) { return 0 == i % 2; };

    std::vector<int> temp;    
    std::copy_if(begin(numbers), end(numbers), std::back_inserter(temp), even);
    std::vector<int> temp2(begin(temp)+1, end(temp));
    
    for (auto iter = rbegin(temp2); iter!=rend(temp2); ++iter)
        std::cout << *iter << ' ';                                  
}

到 Compiler Explorer 上运行它。

以上代码执行以下步骤:

  • 它用所有的偶数来创建 temp,
  • 然后,它跳过一个元素并将所有内容复制到 temp2,
  • 最后,它以相反的顺序输出 temp2 中的所有元素。

(*):对于 temp2,我们可以在最后一个元素之前停止反向迭代,但这需要首先找到最后一个元素,所以让我们坚持使用一个临时容器的简单版本…

(*):本文的早期版本包含了一个不同的示例,它跳过了前两个元素,但这不是最好的一个,我对它进行了修改(感谢各种评论)。

我特意使用名称 temp 和 temp2 来表示代码必须执行输入序列的额外副本。

现在让我们用范围(Ranges)库来重写它:

#include <algorithm>
#include <vector>
#include <iostream>
#include <ranges>   // new header!

int main() {
    const std::vector numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    auto even = [](int i) { return 0 == i % 2; };
 
    using namespace std::views;
    auto rv = reverse(drop(filter(numbers, even), 1));
    for (auto& i : rv)
        std::cout << i << ' ';                                 
}

到 Compiler Explorer 上运行它。

哇!非常地 nice!

这一次,我们有一个完全不同的计算模型:我们没有创建临时对象并逐级执行算法,而是将逻辑包装到组合视图中。

在讨论代码之前,我应该引入两个基本主题,并对它们进行松散定义,以获得基本的直觉:

范围(Ranges)—— 范围是一种抽象,允许 C++ 程序对数据结构的元素进行统一操作。我们可以把它看作是对两个迭代器的泛化。最简单的范围应该定义了 begin() 和 end() 元素。有几种不同类型的范围:容器(containers)、视图(views)、大小范围(sized ranges)、假借范围(borrowed ranges)、双向范围(bidirectional ranges)、向前范围(forward ranges)等等。

容器(Container)—— 它是一个包含有元素的范围。

 视图(View)—— 它是一种不拥有其 begin/end 所指向的元素的范围。视图的创建、复制和移动成本很低。

 我们的代码做以下工作(由内而外):

  • 我们从 std::views::filter 开始,它额外接受一个谓词 even,
  • 然后,我们添加 std::views::drop(从上一步中删除一个元素),
  • 最后一个视图是在以上应用 std::reverse 视图,
  • 最后一步是获取该视图并在循环中遍历它。

你能看出区别吗?

视图 rv 在创建时不做任何工作。我们只填写最后的“收据”。只有当我们迭代它时,执行才会惰性地发生。

字符串左侧除空并转大写

让我们再看一个字符串处理的例子:

下面是标准版本:

const std::string text { "    Hello World" };
std::cout << std::quoted(text) << '\n';

auto firstNonSpace = std::find_if_not(text.begin(), text.end(), ::isspace);
std::string temp(firstNonSpace, text.end());
std::transform(temp.begin(), temp.end(), temp.begin(), ::toupper);
    
std::cout << std::quoted(temp) << '\n';

到 Compiler Explorer 上运行它。

接下来是范围库版:

const std::string text { "    Hello World" };
std::cout << std::quoted(text) << '\n';

auto conv = std::views::transform(
        std::views::drop_while(text, ::isspace), 
        ::toupper 
    );

std::string temp(conv.begin(), conv.end());

std::cout << std::quoted(temp) << '\n';

到 Compiler Explorer 上运行它。

这次,我们将 views::drop_while 与 views::transform 组合在一起。之后,一旦视图准备好了,我们就可以迭代并构建最终的 temp 字符串。

范围视图和范围适配器对象

示例使用了来自 std::views 名称空间的视图,该名称空间定义了一组预定义的范围适配器对象。这些对象和管道操作符允许我们使用更短的语法。

根据 C++ Reference:

如果 C 是一个范围适配器对象,R 是一个 viewable_range 对象,那么这两个表达式是等价的:C(R) 和 R | C。

适配器通常创建一个 ranges::xyz_view 对象。例如,我们可以将初始示例重写为:

std::ranges::reverse_view rv{ 
	std::ranges::drop_view { 
		std::ranges::filter_view{ numbers, even }, 1 
	}
};

代码等效于我们的 std::views::… ,但情况可能更糟,如下所示:

std::views::xyz 与 ranges::xyz_view 孰优孰劣?

我们应该使用 std::views::drop 还是显式 std::ranges::drop_view?

它们有何区别?

为了理解其中的区别,我想引用 Barry Revzin 的一篇博客文章的片段。 

对比:

auto a = v | views::transform(square);
auto b = views::transform(v, square);
auto c = ranges::transform_view(v, square);

他声称 views::transform(或更通用的 views::meow)是一种面向用户的算法,应该优先于选项 c (应该考虑实现细节)。

例如,views::as_const 生成对象的 const 视图。对于 int& ,它构建了 const int& 对象的视图。但如果你传递的是已经是 const int&,那么这个视图会返回初始视图。因此 views::meow 通常更聪明,可以比 ranges::meow_view 做出更多的选择。

ranges::meow_view 唯一合理的用例是当您实现另一个自定义视图时。在这种情况下,最好直接“generate” ranges::meow。

总是更喜欢 views::meow 而不是 ranges::meow_view,除非你有非常明确的理由特别需要使用后者——这几乎肯定意味着你正在实现一个视图,而不是使用一个视图。

 C++23(尚未支持)

您可能注意到,我仍然需要一个额外的步骤来从视图构建最终的字符串。这是因为范围在 C++ 20中是不完整的,我们将在 C++ 23 中得到更多方便的东西。

C++23 最突出和最方便的特性之一是 ranges::to。简而言之,我们将能够编写std::ranges::to<std::string>();因此,代码将变得更加简单:

#include <algorithm>
#include <vector>
#include <iostream>
#include <iomanip>
#include <ranges>

int main() {
    const std::string text { "   Hello World" };
    std::cout << std::quoted(text) << '\n';
    
    auto temp = text | 
                std::views::drop_while(isspace) | 
                std::views::transform(::toupper) | 
                std::ranges::to<std::string>();

    std::cout << std::quoted(temp) << '\n';
}

你可以在MSVC的最新版本中尝试一下:Compiler Explorer。

现在,temp 是一个从视图创建的字符串。算法的组合和其他容器的创建将变得更加简单。

预定义的视图

下面是 C++20 中预定义视图的列表:

名称含义
views::all

返回一个包含传入的 range 参数的所有元素的视图。

filter_view/filter返回满足谓词的基础序列元素的视图。
transform_view/transform在对每个元素应用转换函数后,返回底层序列的视图。
take_view/take返回来自另一个视图的前N个元素的视图,如果改编后的视图包含的元素少于N,则返回所有元素。
take_while_view/take_while给定一个一元谓词 pred 和一个视图 r,它生成一个范围 [begin(r), ranges::find_if_not(r, pred)) 的视图。
drop_view/drop返回一个从另一个视图中排除前 N 个元素的视图,如果改编后的视图包含少于 N 个元素,则返回一个空范围。
drop_while_view/drop_while给定一个一元谓词 pred 和一个视图 r,它生成一个范围 [ranges::find_if_not(r, pred), ranges::end(r)) 的视图。
join_view/join

它将一个范围视图平展为一个视图

split_view/split它接受一个视图和一个分隔符,并将视图划分为分隔符上的子范围。分隔符可以是单个元素,也可以是元素的视图。
counted计数视图显示了迭代器i和非负整数 n 的计数范围([iterator.requirements.general]) i+[0, n) 的元素的视图。
common_view/common获取一个迭代器和哨兵具有不同类型的视图,并将其转换为具有相同类型迭代器和哨兵的相同元素的视图。对于调用期望范围的迭代器和哨点类型相同的遗留算法很有用。
reverse_view/reverse它采用一个双向视图并生成另一个视图,该视图以相反的顺序迭代相同的元素。
elements_view/elements它接受一个类元组值和 size_t 的视图,并生成一个值类型为已改编视图值类型的第 n 个元素的视图。
keys_view/keys获取一个类元组值的视图(例如 std::tuple 或 std::pair),并生成一个值类型为已改编视图的值类型的第一个元素的视图。它是elements_view<views::all_t<R>, 0> 的别名。
values_view/values获取一个类元组值的视图(例如std::tuple 或 std::pair),并生成一个值类型为已改编视图的值类型的第二个元素的视图。它是elements_view<views::all_t<R>, 1>的别名。

您可以在标准的这一部分中阅读它们的详细信息:[range.factories]

另外,从c++ 23开始,我们将新增以下视图:

名称含义
repeat_view/views::repeat由重复产生相同值的生成序列组成的视图
cartesian_product_view/views::cartesian_product一种由由n元笛卡尔积计算出的结果元组组成的视图
zip_view/views::zip由对已改编视图的相应元素的引用的元组组成的视图
zip_transform_view/views::zip_transform一种由转换函数应用到所适应视图的相应元素的结果元组组成的视图
adjacent_view/views::adjacent由对已改编视图的相邻元素的引用元组组成的视图
adjacent_transform_view/views::adjacent_transform一种视图,由转换函数应用于所适应视图的相邻元素的结果元组组成
join_with_view/views::join_with一种视图,由将范围视图平展得到的序列组成,元素之间有分隔符
slide_view/views::slide第 M 个元素是另一个视图的第 M 个到 (M + N - 1) 个元素的视图
ranges::chunk_view/views::chunk一个由另一个视图的元素组成的n个大小的不重叠连续块的视图范围
ranges::chunk_by_view/views::chunk_by将视图拆分为给定谓词返回false的每对相邻元素之间的子范围
ranges::as_const_view/views::as_const

将视图转换为常量范围

ranges::as_rvalue_view/views::as_rvalue将每个元素强制转换为右值的序列视图
ranges::stride_view/views::stride由另一个视图的元素组成的视图,一次向前移动N个元素

详见:Ranges library (C++20) - cppreference.com

总结 

在这篇博文中,我只是简单介绍了 C++20 的范围库(甚至快速浏览了 C++23)。

如你所见,这个想法很简单:将迭代器包装成一个单一的对象——一个范围,并提供一个额外的抽象层。尽管如此,就像一般的抽象一样,我们现在得到了许多新的强大的技术。计算模型因算法组合而改变。与其分步骤执行代码并创建临时容器,不如构建一个视图并执行一次。

你开始使用范围了吗?你最初的经历是什么?请在文章下方的评论中告诉我们。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++20 范围库:关键优势——算法的组合 的相关文章

  • 5G/NR 5G核心网(5GC)之漫游参考架构

    系列文章目录 5G NR 5G核心网 5GC 之基本概念 5G NR 5G核心网 5GC 之网络功能和实体 5G NR 5G核心网 5GC 之非漫游参考架构 5G NR 5G核心网 5GC 之网络功能服务通信 1 5GS漫游参考架构 5G架
  • axure 表格自适应界面

    思路和效果如下图 xff1a 在给定宽度 xff08 all from xff09 内 xff0c 给列表百分比宽度 xff0c 后续修改中 xff0c 只需要修改all from xff0c all from y xff0c 既可调整大小
  • STM32MCU量产工具:STM32 Batch Programmer

    写在最前面 由于本人从事嵌入式开发 xff08 STM32方向 xff09 xff0c 公司产品多数都为STM32芯片 xff0c 面对批量芯片烧录工作实在麻烦 xff08 未送寄焊厂烧录 xff09 xff0c 于是本人根据公司 同事及自
  • QNX 7.1 交叉编译 boost 1.76

    配置 QNX 编译环境 假如 QNX 7 1 SDP 的路径为 work sdk qnx710 xff0c 使用 source 命令让 qnx 环境变量生效 xff1a span class token builtin class name
  • 创建自签名数字证书PFX格式

    创建自签名数字证书PFX格式 1 在系统中安装一个自签名数字证书 1 1 安装makecert exe xff0c 可直接安装Visual Studio xff08 Visual Studio包含makecert exe xff09 1 2
  • E5 调用API续订服务:Microsoft 365 E5 Renew X

    Microsoft 365 E5 Renew X Microsoft 365 E5 Renew X是一款网页版的E5续订服务 xff0c 其依赖网页浏览器呈现支持用户多端操作 xff0c 完全将E5账户API调用托管在了服务器端因此用户无需
  • ARM Cortex-M3 深度研究 - 慎用 volatile 关键字修饰 double longlong 等64位长度类型的变量

    ARM Cortex M3 深度研究 慎用 volatile 关键字修饰 double longlong 等64位长度类型的变量 导语 大家在做嵌入式项目的时候应该都使用过volatile关键字来修饰访问比较频繁的变量 volatile关键
  • 综合项目之闪讯破解(二)之 如何用C++建立PPPOE连接

    如何用C 43 43 建立PPPOE连接 之前我们把闪讯拨号的真实用户名通过特定算法得到了 xff0c 密码闪讯是没有加密的 xff0c 因此我们接下来就是建立一个PPPOE连接 通过程序代码建立PPPOE连接的方法有大概这么几种 xff1
  • 综合项目之闪讯破解(三)之 如何用C++实现PPPOE拨号

    如何用C 43 43 实现PPPOE拨号 1 xff09 CMD命令行方式 拨号本是一件很简单的事情 xff0c CMD命令行直接搞定 xff0c 但我在第一篇文章中提到过 xff0c 闪讯的真实用户名的前两位是换行符 n 和 r xff0
  • 综合项目之闪讯破解(四)之 如何用C++编写可被C#调用的Dll

    如何用C 43 43 编写可被C 调用的Dll 之前的三篇文章已经将闪讯的算法以及PPPOE拨号的关键函数全部用C 43 43 实现了 xff0c 但C 43 43 写界面比较繁琐 xff0c 本身我擅长C 所以决定用C 写界面 xff0c
  • 综合项目之闪讯破解(五)之 如何用C#调用C++编写的Dll

    如何用C 调用C 43 43 编写的Dll 上一篇文章我们提到了C 与C 43 43 DLL的联合项目的好处 xff0c 并且成功编写了一个求和函数的C 43 43 Dll 接下来继续用C 调用这个Dll 1 xff09 用VS建立一个C
  • MBR+BIOS与GPT+UEFI启动系统的区别

    MBR 43 BIOS与GPT 43 UEFI启动系统的区别 近些年来许多电脑厂商的出厂电脑都自带系统 xff0c 这些电脑的分区看起来和我们一般的组装机电脑的分区有些不同 xff0c 它除了有一个系统主分区之外在主分区之前还有3个小分区
  • 如何装GPT+UEFI / MBR+BIOS的系统

    本文将深度讲述装机步骤与原理 xff0c 解决装机过程中遇到的各种问题 xff0c 使用不同的方式装机 本文所有的装机方式笔者都亲自尝试过 xff0c 所有重装步骤不会损毁除系统盘外其他分区文件 这是笔者总结许久的装机经验 xff0c 本文
  • 综合项目之闪讯破解(六)之 如何解决程序/C++Dll的兼容性问题

    如何解决程序 C 43 43 Dll的兼容性问题 本文将尝试解决程序与DLL在不同客户机上运行存在的兼容性问题 前言 前面的五篇文章已经将程序的核心部分全部搞定 xff0c 由于我们调用了一些系统API xff0c 所以这很有可能导致程序在
  • Ubuntu 修改 apt 源为阿里云源

    文章目录 软件源 Software Source使用说明图形界面 xff08 新手推荐 xff09 手动修改配置文件 相关链接 软件源 Software Source 软件源是指用于存储各种 Linux 应用程序的存储库 xff0c 包括自
  • C#如何自绘滚动条控件

    C 如何自绘滚动条控件 滚动条样式 普通情况 鼠标位于滑块上面 鼠标按下滑块 程序变量与函数层级图 垂直滚动条代码 不讲如何编写了 代码上面有注释 直接上代码片了 span class hljs comment 自定义控件 xff1a 垂直
  • TeeChart WPF DLL消除水印

    TeeChart WPF DLL消除水印 准备工作 Steema TeeChart for NET 2017 Evaluation 4 1 2017 03147 DNSPY X86 一些代码 xff1a Texts ExpireMsg1 W
  • polybar基础使用

    如果不喜欢i3或者其他桌面系统的默认状态栏 xff0c polybar还是一个不错的选择 基本上 xff0c 所有的流行的Linux发行版都可以用包管理器直接进行polybar的安装 ArchLinux可以使用yay或者其他AUR工具安装
  • ArchLinux图形界面安装与美化:i3+polybar

    arch只提供了tty xff0c 我们需要自己配置一个漂亮的桌面 你可以选择继承好的GNOME Deepin等桌面 但是他们都包含了许多你可能并不能用到的工具和插件 i3wm是一个窗口管理器 xff0c 它提供了最基础的窗口管理功能 你可
  • ceph学习(1)——手动部署ceph分布式存储集群(使用本地源离线安装),一次成功!!

    手动搭建适合已经使用自动部署工具成功搭建了集群 xff0c 想加深对ceph理解的同学 xff0c 手动搭建ceph可以更灵活的配置ceph集群 xff0c 现在就开始吧 xff01 xff01 一 环境准备 1 服务器配置 xff1a 三

随机推荐

  • iOS开发 - touchBegan事件判断点击的位置在View上还是在View的子View上

    span class hljs pp span class hljs params style color rgb 102 0 102 void span touchesBegan span class hljs params style
  • SQL Server解析Json格式数据

    主要介绍5个函数 xff1a openJson 打开Json字符串IsJson 判断一个字符串是不是合法的Json格式 是返回1 xff0c 否返回0 xff0c null返回null Json Value 从Json字符串中提取值 Jso
  • openwrt 无线桥接(AP)

    无线桥接常见的有三种模式 1 AP模式 AP模式其实是Access Point的简称 xff0c 意思是 xff1a 访问接入点 而无线网桥的AP模式 xff0c 也就是利用无线网桥做无线信号的接入点了 那么 xff0c 究竟是什么东西接入
  • macOS下使用anaconda相关系列

    创建虚拟环境 conda create n 环境名 python 61 3 6 进入虚拟环境 source bash profile source activate 环境名 其中bash profile是安装anaconda时候默认生成的环
  • Windows 安装NET4.6/4.7/4.8 时间戳签名和或证书无法验证或已损坏

    时间戳签名和或证书无法验证或已损坏 问题版本 xff1a 事件 xff1a 解决办法下载补丁程序 xff1a 安装KB2813430补丁 注意事项补丁需要重启设备生效 2021 10 11 by 崔斐然 问题 版本 xff1a win7专业
  • 在 Ubuntu Linux 中使用 PPA(完全指南)

    译 xff1a 在 Ubuntu Linux 中使用 PPA xff08 完全指南 xff09 作者 xff1a Abhishek Prakash 自由和开放源码软件的创造者 一个热心的 Linux 用户和开源推动者 从阿加莎 克里斯蒂和夏
  • RDP(远程桌面)优化

    RDP连接优化 一 优化连接时间二 优化集显帧率三 开启RemoteFX USB重定向 xff08 如果有需要 xff09 四 MacOS系统RDP超高清显示 2022 03 31 by 崔斐然 一 优化连接时间 1 客户端 xff1a 关
  • 【FRP】windowsServer部署FRP

    FRP windowsServer部署FRP 1 下载FRP nssm2 服务器端部署过程 xff1a 3 客户端部署过程 xff1a 4 卸载服务 2022 08 24 by 崔斐然 1 下载FRP nssm 下载地址 xff1a FRP
  • 【FRP】群晖docker中部署Frp

    2022 08 24 by 崔斐然 0 xff1a 需求 公司有台笔记本 xff0c 现在疫情期间居家办公 我用的MacBook RDP客户端做的非常好用 xff0c 如相互粘贴文件 文字等 xff0c MacBook通过远程桌面连接公司内
  • Debian 9/10快速开启Google BBR的方法,实现TCP高效单边加速

    BBR 是谷歌公司的某个员工研发出来的服务器单边加速算法 xff0c Linux内核从4 9版开始集成BBR算法 相比锐速BBR的加速效果更为温和 xff0c 并且占用内存小对服务器压力也很小 xff0c 当时理想情况下是可以跑满整个服务器
  • 基于机器学习的捡球机器人设计与实现(探索)第4篇——机械设计)

    2019 03 18 by 崔斐然 原以为软件很复杂 机械好搞 结果发现 都难搞 一次次想出办法又一次次被自己否定 我tm想静静
  • 人脸识别之Hog特征+SVM分类器训练与使用

    原文来自 xff1a https juejin im post 5b0e70686fb9a00a1451c8e7 计算机视觉 人脸识别 xff08 Hog特征 43 SVM分类器 xff09 一 SVM支持向量机 1 SVM原理 在机器学习
  • python利用PIL实现对图片截图

    在对图像处理时 xff0c 我们有时候需要对图片某区域进行截图 xff0c 话不多说 xff0c 直接上代码 xff1a from PIL import Image import sys 先将 input image 填充为正方形 def
  • PowerMock介绍和用法

    PowerMock PowerMock简介一 PowerMock xff1f 二 Mock底层原理1 Mockito2 PowerMock原理 三 应用场景1 依赖问题 xff0c 打桩 2 工程质量 PowerMock使用步骤一 添加依赖
  • Windows10 WSL2磁盘迁移

    一 使用 WSL 命令行工具 在 Windows 10 版本 1903 xff08 2019 年 4 月更新 xff09 或更高版本中 xff0c 您可以使用wsl exe命令行工具 1 导出分布 使用要移动的分发创建一个 tar文件wsl
  • linux下搭建confluence

    一 Java环境 java环境 二 mysql 2 1 安装前的检查和准备工作 2 1 1检查 1 是否安装过mysql xff1a rpm qa grep mysql 2 如果有的话 xff0c 就删除 xff08 XXXX是自己的mys
  • 译:SOME/IP 技术细节

    译 xff1a SOME IP 技术细节 原文 SOME IP technical details SOME IP Scalable service Oriented MiddlewarE over IP 基于 IP 可扩展面向服务中间件
  • Python requests_toolbelt的使用

    multipart form data Encoder The main attraction is a streaming multipart form data object MultipartEncoder Its API looks
  • ArchLinux中文安装教程

    以自己的电脑安装为参考 xff0c 已安装win10系统 最后效果为win10和arch双系统 xff01 xff01 xff01 一 准备工作 1 按照实际需要划分出一部分空闲磁盘空间 xff0c 右击想要安装arch的分区点击删除卷 x
  • C++20 范围库:关键优势——算法的组合

    从概念上讲 xff0c 范围 xff08 Range xff09 是一个简单的概念 xff1a 它只是一对迭代器 指向序列的开始和结束 xff08 在某些情况下是一个哨兵 xff09 然而 xff0c 这样的抽象却可以从根本上改变编写算法的