前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >C++尝鲜:在C++中实现​​​LINQ!

C++尝鲜:在C++中实现​​​LINQ!

作者头像
腾讯云开发者
发布于 2022-06-24 10:08:00
发布于 2022-06-24 10:08:00
2K00
代码可运行
举报
运行总次数:0
代码可运行

导语 | 在正式分析libunifex之前,我们需要了解一部分它依赖的基础机制,方便我们更容易的理解它的实现。本篇介绍的主要内容是关于c++ linq的,可能很多读者对c++的linq实现会比较陌生,但说到C#的linq,大家可能马上就能对应上了。没错,c++的linq就是在c++下实现类似C# linq的机制,本身其实就是在定义一个特殊的DSL,相关的机制已经被使用在c++20的ranges库,以及不知道何时会正式推出的execution库中,作为它们实现的基础之一。本篇我们主要围绕已进入标准的ranges实现来展开关于c++ linq的探讨,同时也将以ranges的一段代码为起点,逐步展开本篇的相关内容。

一、从ranges示例说起

ranges是c++20新增的特性,很好的弥补了c++容器和迭代器实现相对其他语言的不便性。它的使用并不复杂。我们先来看一个具体的例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
auto const ints = { 0, 1, 2, 3, 4, 5 };auto even_func = [](int i) { return i % 2 == 0; };auto square_func = [](int i) { return i * i; };auto tmpv = ints   | std::views::filter(even_func)   | std::views::transform(square_func);
for (int i : tmpv) {  std::cout << i << ' ';}

初次接触, 相信很多人都会疑惑:

  • 这是如何实现的?
  • c++里也能有linq?
  • 为什么这种表达虽然其他语言常见, 在c++里存在却显得有点格格不入?

从逻辑上来讲, 上述代码起到的是类似语法糖的效果, linq表达: ints | std::views::filter(even_func) | std::views::transform(square_func);

等价函数调用方式为: std::views::transform(std::views::filter(ints, event_func), square_func);

所以表面上来看,它似乎是通过特殊的|操作符重载来规避掉了多层函数嵌套表达,让代码有了更好的可读性,表达更简洁了。

但这里的深层次的设计其实并没有那么简单,这也是大家读ranges相关的文章,会发现这“语法糖”居然还会带来额外的好处,最终compiler生成的目标代码相当简洁。这是为什么呢?我们将在下一章中探讨这部分的实现机制。

二、特殊的DSL实现

其实本质上来说, 这种实现很巧妙的利用了部分compiler time的特性,最终在c++中实现了一个从“代码->Compiler->Runtime”的一个DSL,后续我们也介绍到,execution里也复用并发扬了这种机制。我们先来看一下ranges这部分的机制:

  • DSL定义(BNF组成)-首先是范式的组成,ranges的linq用到的范式比较简单,我们可以认为,它是由Ranges Pipeline::=Data Source { '|' Range Adapter } '|' Range Adapter组成的。
  • Compiler(Pipeline操作)-ranges实现里我们可以认为|运算的过程就是编译过程。
  • Execute-具体的iterator过程,ranges里一般就是std::ranges::begin(),std::ranges::end(),以及iterator本身所支持的++操作等。

这种设计本身带来的好处,对比原始的容器和迭代器操作,Compiler部分和Execute过程被显示分离了,Compiler的时候,并不会对Data Source做任何的访问和操作,所有访问相关的操作其实是后续Execute过程再发生的(Lazy特性)。

另外,因为Compiler过程本身是结合comipler time特性来处理的,这样DSL本身在这个阶段是类型完备的,一方面compiler过程本身就能完成一些常规的类型匹配问题检查等操作,另外我们也能在该阶段在类型完备的情况下更好的处理相关逻辑。

大量使用compiler time特性带来的额外好处是原始的std容器和迭代器很多在运行时进行处理的操作,都可以在编译期完成,编译器会生成比原来运行效率高效很多的代码。

像这种设计精巧,系统性完备,优势又很明显的机制,必然会得到发扬光大。所以我们会看到,ranges库本身使用了相关机制,到几经迭代尚未正式推出的execution库,都已经拥抱了这种设计,将其作为自己基础的一部分,作为sender/receivers机制的基石,相关的实现也被越来越多的c++ coder所认可。

本篇我们还是回到ranges本身,先关注Compiler部分也就是Pipeline机制实现的细节,以微软官方的ranges实现为例,一起来详细了解一下它的实现机制。

三、pipeline机制浅析

(一)Pipe实现相关的concept

  • _Pipe::_Can_pipe
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
namespace _Pipe {  template <class _Left, class _Right>  concept _Can_pipe = requires(_Left&& __l, _Right&& __r) {    static_cast<_Right&&>(__r)(static_cast<_Left&&>(__l));  };}

这个concept比较简洁,能够组织成pipe的对象,以

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
auto pipe = l | r;

为例,能够以r(l)的形式调用的两个对象,即可满足pipe约束。

  • _Can_compose
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
namespace _Pipe {  template <class _Left, class _Right>  concept _Can_compose = constructible_from<remove_cvref_t<_Left>, _Left>    && constructible_from<remove_cvref_t<_Right>, _Right>;}

这个主要是因为lazy evaluate的过程中,我们可能需要在中间对象中(如下文中的_Pipeline对象),对_Left和_Right进行存储,所以需要它们是可构建的。

(二)Pipe实现相关的类

  • struct _Base\<class _Derived\>类

相关源代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
namespace _Pipe {
  template <class, class>  struct _Pipeline;
  template <class _Derived>  struct _Base {    template <class _Other>    constexpr auto operator|(_Base<_Other>&& __r) &&  {      return _Pipeline{static_cast<_Derived&&>(*this), static_cast<_Other&&>(__r)};    }
    template <class _Other>    constexpr auto operator|(const _Base<_Other>& __r) &&  {      return _Pipeline{static_cast<_Derived&&>(*this), static_cast<const _Other&>(__r)};    }
    template <class _Other>    constexpr auto operator|(_Base<_Other>&& __r) const& {      return _Pipeline{static_cast<const _Derived&>(*this), static_cast<_Other&&>(__r)};    }
    template <class _Other>    constexpr auto operator|(const _Base<_Other>& __r) const& {      return _Pipeline{static_cast<const _Derived&>(*this), static_cast<const _Other&>(__r)};    }
    template <_Can_pipe<const _Derived&> _Left>    friend constexpr auto operator|(_Left&& __l, const _Base& __r)    {      return static_cast<const _Derived&>(__r)(_STD forward<_Left>(__l));    }
    template <_Can_pipe<_Derived> _Left>    friend constexpr auto operator|(_Left&& __l, _Base&& __r)    {      return static_cast<_Derived&&>(__r)(_STD forward<_Left>(__l));    }  };}

以微软版range库的实现为例,各个range adapter-如std::views::filter,std::views::transform等都继承自_Base类,_Base类主要完成以下两个功能:

  • 完成对其它_Base类的管道操作。
  • 通过友元和模板来完成对其它类的管道操作(自己作为右操作数)
  • 具体的重载不再具体展开了,主要是不同_Right类型的差异处理,可自行参阅相关代码。
struct _Pipeline<class _Left,class _Right>类

相关代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template <class _Left, class _Right>struct _Pipeline : _Base<_Pipeline<_Left, _Right>> {  _Left __l;  _Right __r;
  template <class _Ty1, class _Ty2>  constexpr explicit _Pipeline(_Ty1&& _Val1, _Ty2&& _Val2)    : __l(std::forward<_Ty1>(_Val1))    , __r(std::forward<_Ty2>(_Val2)) {  }
  template <class _Ty>  constexpr auto operator()(_Ty&& _Val)     requires requires {    __r(__l(static_cast<_Ty&&>(_Val)));    }  { return __r(__l(_STD forward<_Ty>(_Val))); }
  template <class _Ty>  constexpr auto operator()(_Ty&& _Val) const     requires requires {    __r(__l(static_cast<_Ty&&>(_Val)));    }  { return __r(__l(std::forward<_Ty>(_Val))); }};
template <class _Ty1, class _Ty2>_Pipeline(_Ty1, _Ty2) -> _Pipeline<_Ty1, _Ty2>;

_Pipeline主要用于将两个range adapter进行复合的情况,比如下面的情况:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
auto v = std::views::filter(even_func) | std::views::transform(square_func);

这个时候我们会构建_Pipeline对象, 区别于这种情况则是不依赖中间_Pipeline对象, 比如下面的情况:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
auto ints = {1, 2, 3, 4, 5};auto v = ints | std::views::filter(even_func);

这种情况 , 我们就不需要依赖_Pipeline对象, 直接触发的是_Pipe这个版本的operator|重载:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template <_Can_pipe<_Derived> _Left>friend constexpr auto operator|(_Left&& __l, _Base&& __r){  return static_cast<_Derived&&>(__r)(_STD forward<_Left>(__l));}

std::views::filter本身是一个CPO closure对象,不理解CPO没关系,下篇中将进行具体介绍,我们可以先将它简单理解成一个带up value的函数对象,上例中的even_func被携带到了一个std::views::filter CPO对象中, 然后我们可以以 filter_cpo(ints) 的方式来产生一个预期的views,cpo的这个特性倒是跟其他语言的closure特性基本一致,除了C++的CPO对象比较Hack,使用形式不如其他语言简洁外。

另外需要关注的一点是:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template <class _Ty1, class _Ty2>        _Pipeline(_Ty1, _Ty2) -> _Pipeline<_Ty1, _Ty2>;

这个是c++17添加的Custom template argument deduction rules(或者user-defined template argument deduction rules),利用用户自行指定的推导规则,我们可以使用简单的_Pipeline(a,b)来替换_Pipeline<a,b>(),以得到更简单的表达,如_Base类中的使用一样:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
_Pipeline{static_cast<const _Derived&>(*this), static_cast<_Other&&>(__r)};

四、总结

本篇中我们简单介绍了c++ linq,以及ranges中相关机制的使用,也侧重介绍了作为linq Compiler部分的Pipeline的具体实现。但可能有细心的读者已经发现了,ranges中的各种range adapter-如std::views::transform()和std::views::filter()的实现,好像跟自己之前见到的惯用的C++封装方式不太一样,这也是我们下一篇中将介绍的内容。

参考资料:

1.ranges-cppreference

 作者简介

沈芳

腾讯后台开发工程师

IEG研发效能部开发人员,毕业于华中科技大学。目前负责CrossEngine Server的开发工作,对GamePlay技术比较感兴趣。

 推荐阅读

C++异步从理论到实践!

C++反射:反射信息的自动生成!

C++反射:全方位解读Lura库的前世今生!

小白入门级!webpack基础、分包大揭秘

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯云开发者 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
C#编写影院售票系统(A project with a higher amount of gold )(1:项目需求 ,思路分析与窗体效果)
此篇文章为项目需求 ,思路分析与窗体效果,,,需要相关代码请访问:http://www.cnblogs.com/lsy131479/p/8367314.html 项目需求: 影院售票系统 1.基础设施 放映厅 座位集合 2.一个海报------------>放映计划 3.售票设置----------->观影 领域模型:程序中提炼出的实体 4.从电影Movie开始 movieName 影片名称 Poster    海报 Director  导演 Actor  主演 movieType 影片类型 枚举类型 Pr
房上的猫
2018/03/14
1.2K0
C#编写影院售票系统(A project with a higher amount of gold )(1:项目需求 ,思路分析与窗体效果)
C#编写街道管理系统
项目需求: 一、语言和环境A、实现语言 C#B、环境要求 Visual Studio 2012 二、功能要求 现使用.NET WinForms技术为居委会开发一个街道管理软件,其中街道管理窗体界面如图-4所示。 要求: 1、读取提供的XML文件(详见 \提供给学员的素材\Address.xml),将其信息动态添加到TreeView控件中。 2、创建表示地址信息的实体类,并使用泛型集合保存XML文件中的信息。 3、选中某子节点,能够将相关信息显示到右侧的文本框中,如图-4所示。 4、窗体标题为“街
房上的猫
2018/03/14
1.4K0
C#编写街道管理系统
亲自上手,用原生 JavaScript 打造简易电影选座系统
首先,我们需要一个基础的HTML结构来展示电影列表和座位布局。这一步主要是定义页面的基本框架,让用户能够看到选择电影和座位的界面。
前端达人
2024/06/14
5090
亲自上手,用原生 JavaScript 打造简易电影选座系统
C#中常用的几种读取XML文件的方法
本文转载:http://www.cnblogs.com/xiaoxiangfeizi/archive/2011/07/29/2120807.html
跟着阿笨一起玩NET
2018/09/18
6.2K0
C#中常用的几种读取XML文件的方法
C#开发BIMFACE系列23 服务端API之获取模型数据8:获取模型链接信息
在Revit等BIM设计工具中可以给模型的某个部位添加链接信息。即类似于在Office Word、Excel 中给一段文字添加本地文件链接或者网址链接等类似功能。例如下面的一个RVT模型种包含了2个链接。
张传宁IT讲堂
2019/09/18
5480
C#开发BIMFACE系列23 服务端API之获取模型数据8:获取模型链接信息
C#开发BIMFACE系列14 服务端API之批量获取转换状态详情
上一篇《C#开发BIMFACE系列13 服务端API之获取转换状态》中介绍了根据文件ID查询单个文件的转换状态。
张传宁IT讲堂
2019/09/18
6020
常用的Hadoop 文件查看工具
packages.config <?xml version="1.0" encoding="utf-8"?> <packages> <package id="Microsoft.AspNet.W
挖掘大数据
2018/01/15
2K0
分享 | C#编写的电影售票系统(附源码+数据库)
最近疫情真的很可怕,大家要注意保护好自己,响应国家的号召。尽量不出门,可以利用这个时间好好充实自己。
短短的路走走停停
2020/02/25
4.9K15
C#开发BIMFACE系列17 服务端API之获取模型数据2:获取构件材质列表
在上一篇《C#开发BIMFACE系列16 服务端API之获取模型数据1:查询满足条件的构件ID列表》中介绍了获取单文件(模型)的所有构建ID列表。每个构建由多种材质组成,本文介绍获取单个构建ID的材质列表。
张传宁IT讲堂
2019/09/18
4070
C#开发BIMFACE系列17 服务端API之获取模型数据2:获取构件材质列表
移动开发(七):.NET MAUI使用RESTAPI实现查询天气笔记
在移动开发过程中,第三方对接是非常常见的。今天给大家分享.NET MAUI如何使用REST API实现输入城市名称查询天气的示例,希望对大家学习.NET MAUI可以提供一些帮助!
小明互联网技术分享社区
2024/09/24
3090
移动开发(七):.NET MAUI使用RESTAPI实现查询天气笔记
简单代码生成器原理剖析(一)
上篇文章(深入浅出三层架构)分析了简单三层架构的实现。包括Model,DAL(数据访问层),BLL(业务逻辑层)的实现。 实际开发中,由于重复代码的操作,会花费大量时间,如果以代码生成器来自动生成三层
用户1161731
2018/01/11
1.4K0
简单代码生成器原理剖析(一)
PHP代做编程辅导:CPT270 Cinemas
Web的裸写大作业,做一个Cinemas,包括Image Gallery,Movies Selection,Movies Reservation,Shopping Cart等等功能页面,简单粗暴耗时,做了整整两天。30个得分点,每个一分,再加上bonus那滋味真是酸爽。
拓端
2022/10/30
6840
代码生成器原理及示例
在三层架构中Model、DAL(Data Access Layer)、BLL层有必要分开,其中有些代码可以由代码生成器生成。虽然网络已经有成熟的代码生成器,但是第三方代码生成器在实际应用场景中,生成的代码经常还需要在其基础上修改。修改其代码就不如修改代码生成器本身。所以掌握代码生成器的编写方法、原理还是很有必要的。
全栈程序员站长
2022/07/25
9900
代码生成器原理及示例
系统设计:在线售票系统
让我们设计一个在线售票系统,销售Ticketmaster或BookMyShow等电影票。
小诚信驿站
2022/03/06
6.7K0
系统设计:在线售票系统
体检套餐管理系统 -- Dictionary<K,V>双列集合
本文章为 Dictionary<K,V>双列集合开发项目,如需要List<T>单列集合开发的此项目,请到楼主博客园寻找 博客网址:http://www.cnblogs.com/lsy131479/ 窗
房上的猫
2018/03/14
9251
体检套餐管理系统 -- Dictionary<K,V>双列集合
Silverlight多重表头实现
效果: 实现主要逻辑:通过动态拼接XML生成表头样式,绑定到列上。 主要是动态拼接XML时要仔细核对对应的占位行,具体可以看代码,注释很详细 两个类一个接口 NTree<T>:定义表头树形结构 1
用户6362579
2019/09/29
1.1K0
Silverlight多重表头实现
C#中返回值封装
      在平时开发过程中常常需要取一个方法的返回值,BOSS写了一个返回值类,做个练习以备不时之需: 返回值支持泛型和非泛型 先贴上代码: 非泛型返回值类: 1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Runtime.Serialization; 6 7 8 namespace WindowsFormsAppli
用户1055830
2018/01/18
1.7K0
C#中返回值封装
体检套餐管理系统 -- List<T>单列集合
本文章为List<T>单列集合开发项目,如需要 Dictionary<K,V>双列集合开发的此项目,请到楼主博客园寻找 博客网址:http://www.cnblogs.com/lsy131479/ 窗
房上的猫
2018/03/14
1.6K1
体检套餐管理系统 -- List<T>单列集合
C# 添加右键菜单
这节内容很简单,本来不打算写的,有群友技术群问了,所以就整理了下写出来了。这节实例是给datagrid添加右键操作菜单,接下来开始正文讲解。
用户9127601
2022/03/23
1.7K0
C# 添加右键菜单
WPF开发之C#中关闭进程的方式
根据名称关闭 使用C#结束 private static void StopNginx() { Process[] processes = Process.GetProcessesByName("nginx"); foreach (Process p in processes) { string basePath = AppDomain.CurrentDomain.BaseDirectory; string nginxPath = System.IO.
码客说
2020/07/28
1.2K0
推荐阅读
相关推荐
C#编写影院售票系统(A project with a higher amount of gold )(1:项目需求 ,思路分析与窗体效果)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验