泛型Lambda,如此强大!

C语言与CPP编程 2022-01-17 08:45
最近发现泛型编程有了另一利器——泛型Lambda,比想象当中要强大许多,威力不小和大家分享一下。
本篇内容需要对泛型编程有所了解,若是读过之前更新的相关文章,食用更佳。

1

泛型编程

开始之前,先来简单回顾一下泛型编程的内容。

泛型编程的目的是将「数据和方法」进行分离,将数据高度抽象,于是可以表示同类问题的「最小通解」。

C++中,通过模板来实现泛型编程,模板又分为变量模板、函数模板和类模板。

这些模板始终围绕着「数据和方法」。变量模板属于对数据类型的抽象,函数模板属于对方法的抽象,而类模板,则二者兼有,因为类本身的目的就是将数据和方法进行结合。

因此为什么说函数模板处理的是数值,而类模板处理的是类型呢?就是由于函数只具有方法,而在C++中方法是不支持偏特化的,所以它无法处理类型。

到了C++14,Lambda也迎来了泛化能力,称为Generic Lambda。不过此时的泛化能力只是由auto带来的,威力略弱。

随后又经过多年的发展,Lambda的能力越来越强。C++20加入了Template Lambda,这让Lambda也可以指定模板参数,使得Lambda的泛化能力更加完善。

至此,C++的泛型编程多了一个新的主角——泛型Lambda。


2

泛型Lambda

为何泛型Lambda值得单独拿出来说呢?

一是因其特殊性,在一些情境使用它来封装变化,会让事情简单许多;二是由其新颖性,它的许多特性和用处尚处探索期,值得讨论。

首先来说其特殊性。

Lambda函数其实就是一个匿名的函数对象,它实际上也是一个”类”。不同的是,它唯一的方法就是operator(),也就是Lambda体,而数据则是[]中捕获的参数,这些参数就是”类”中定义的成员变量。

因此,Lambda函数既具有函数的部分特征,又具有类的部分特征。

也因如此,事情变得有趣起来。

Lambda具有的函数部分特征,让它具备了函数模板的能力;类部分特征,让它具备了类模板的继承能力。

此外,由于Lambda的类型是一个closure type(闭包类型),所以它还可以定义在函数内部,也可以当作回调函数使用。

如此这些,再加上泛型,使得泛型Lambda极具威力。

继而来看其新颖性。

当下大多数C++开发者对于Lambda的使用,还只是停留在函数部分,相当于只发挥了Lambda的基本能力。

实际上,Lambda的能力要比想象之中强大许多,在基本能力之上,还有些令人兴奋的能力。

这也是值得探索的地方。


3

以继承封装「变化」

不论写库或框架,都是在提炼「不变」的逻辑,将「变化」的逻辑交给用户配置。

可以是预留接口,让用户覆写接口;也可以是采用回调,让用户提供处理逻辑;抑或是提供配置文件,让用户填写变化的信息,再通过配置文件自动生成相应处理逻辑。

应对「变化」的方式很多,对于一些逻辑不甚复杂的变化,完全可以借助Lambda来实现。

Lambda天生可以在函数内部构建,自带一个operator (),这就相当于一个表示变化的接口,也就是用户可以手动配置的地方。

有了表示变化的地方,你再将不变的逻辑封装到一个类中,让该类继承自此Lambda。于是,你便可以在不变之中使用变化的逻辑。


4

Lambda重载

既然泛型Lambda具备函数模板的特性,那么它是否也可以重载呢?

回答是no。前文提到,Lambda是函数对象,它只有唯一的一个方法operator(),也就是Lambda体,Lambda体只有一个,你又如何能写多个呢?

但是,可以提供多个Lambda,也就是造就多个函数对象,让它们参数不同,再借助某种技巧,便可以从「视觉层面」实现Lambda重载。

说是「视觉层面」,意思是说它本质上不是函数意义上的那种重载,只是使用起来像是函数重载一样。

这个技巧就是overload pattern。

其实早在C++ DP.13-2 泛化实现Cyclic Visitor与强大的C++17 std::visit这篇文章中,就已经提到并使用了这个技巧。

这里再次拿出一节来介绍它,是因为我发现它比想象之中更加强大,可以说是泛型Lambda编程的一个核心技术。

它的实作很简单,只有两行代码:

1template<class... Tsstruct overloaded : Ts... { using Ts::operator()...; };
2template<class... Tsoverloaded(Ts...) -> overloaded;

各位都知道,C++中代码越少往往并不意味着它有多简单,而是说明其「信息密度」较大。

此处,第一行首先使用了可变参数模板,使得overloaded可以继承自多个Lambda。其次使用Using-declaration,以防止重载之时产生歧义。

第二行则使用了C++17的CTAD(Class Template Argument Deduction),以推导出overloaded的类型。有何必要呢?这是因为你无法创建一个overloaded类型的对象,因为Lambda的类型不可知,你无法填写模板参数类型。借由CTAD,便可以为overloaded添加一个用户自定义的类型推导指引,这样编译器才能够推导出其类型。

现在,就可以使用「视觉层面」的Lambda重载了:

1const auto func = overloaded {
2    [](const int& n) { std::cout << "int:" << n << '\n'; },\
3    [](const std::string& s) { std::cout << "string:" << s << '\n'; }
4};
5
6func(2);
7func("im the lambda with parameter std::string");

这里又使用了「聚合初始化」,通过它可以直接调用基类中Lambda的构造器,从而避免为overloaded显式编写构造函数向基类传递参数。

总而言之,通过Lambda重载,便可以将许多相似的「变化逻辑」聚到一起,再以不同的参数访问这些不同的逻辑,从而以一种崭新的形式封装变化。


5

泛型Lambda实现对象工厂

这一节需要你对C++ DP.08 Factory Method这篇文章有些印象。

通过泛型Lambda,我们拥有了一种新的实现对象工厂的策略,简单而威力巨大。

代码如下:

1template<class... Tsstruct Fruit : Ts... { using Ts::operator()...; };
2template<class... TsFruit(Ts...) -> Fruit;

是的,就是使用了Lambda重载来实现Fruit。

然后再通过以下形式定义对象工厂:

1struct Apple { void print() std::cout << "apple print\n"; } };
2struct Pineapplce {void print() std::cout << "pineapple print\n"; } };
3
4// 定义对象工厂
5static constexpr auto FruitFactory = Fruit {
6    []<typename T>(const T& apple) { return new T; }
7};
8
9// 从工厂创建产品
10auto apple = FruitFactory(Apple{});
11apple->print();

此处第6行代码便使用了C++20的Template Lambda,由此我们可以创建任意类型的对象。

如此少的代码,实现的对象工厂可并不弱,而且这种实现方法更加轻便,除了无法动态产生,已经相当不错了。

6

泛型Lambda实现抽象工厂

这一节需要你对C++ DP.09 Abstract Factory这篇文章有些印象。

没错,根据泛型Lambda,实现抽象工厂也有了一种新的形式。

并且这种形式使用起来更加轻便,我已经决定使用这种方式替换okdp中的实现。

我们可以通过Lambda重载来定义抽象工厂:

1template<class... Tsstruct AbstractAIFactory : Ts... { using Ts::operator()...; };
2template<class... TsAbstractAIFactory(Ts...) -> AbstractAIFactory;

具体工厂的定义则更具有技巧性,实现如下:

1template<class Tclass U>
2concept IsAbstractAI = std:
:same_as;
3
4template<class T>
5static constexpr auto AIFactory = AbstractAIFactory {

6    []() requires IsAbstractAI { return new LuxEasy; },
7    []() requires IsAbstractAI { return new ZiggsEasy; },
8    []() requires IsAbstractAI { return new TeemoEasy; }
9};
10
11auto lux = AIFactory();
12lux->print();

你是否意识到了这种实现形式的强大之处?

这里用到的技术就更加多了,除了前面介绍的「聚合初始化」,还使用到了C++20的Concepts,这点我们已经写过文章了,相信大家不会太陌生。

此外,这里还用到了「变量模板」,想想前面几节的内容提到过范型Lambda虽然具有函数模板和类模板的部分特征,「数部分只能通过捕获参数。因此其实无法真正像类那样使用,而抽象工厂的抽象类又无法实例化,所以我们也无法像对象工厂那样使用。

于是,为了为它添上「类型的能力」,这里借助了变量模板。正因如此,你才能像类一样使用AIFactory。

不过事情尚未结束,此时「抽象工厂」就是AbstractAIFactory,通过Lambda重载完成的不错。「具体工厂」属于变化的部分,就相当于Lambda体,也就是这里为每个类型实现的Lambda函数。

问题在哪呢?巨大的重复!

消除这种类型的重复比较好的方法是借助「泛型宏」,这点在对象工厂那篇文章中介绍并使用过。

泛型编程是理念,模板是手段,宏同样是一种手段,需要根据具体情形具体分析,从而合理地进行选择。

不过此处的情形有些复杂,泛型宏的确可以很好的完成任务,但是工作比较复杂,已经涉及到泛型宏深入层次的技术了。

因此由于本篇主题不是泛型宏,篇幅有限,此处只展示下代码,不做进一步解释,大家可以自己看看。

代码如下:

#define _GET_OVERRIDE(_1, _2, _3, _4, _5, _6, NAME, ...) NAME

#define _CONCRETE_AI_FACTORY_BODY_0(LAM, AIType, LEvel)
#define _CONCRETE_AI_FACTORY_BODY_1(LAM, AIType, Level, AIName) LAM(AIType, Level, AIName)
#define _CONCRETE_AI_FACTORY_BODY_2(LAM, AIType, Level, AIName, ...) LAM(AIType, Level, AIName) _CONCRETE_AI_FACTORY_BODY_1(LAM, AIType, Level, __VA_ARGS__)
#define _CONCRETE_AI_FACTORY_BODY_3(LAM, AIType, Level, AIName, ...) LAM(AIType, Level, AIName) _CONCRETE_AI_FACTORY_BODY_2(LAM, AIType, Level, __VA_ARGS__)
#define _CONCRETE_AI_FACTORY_BODY_4(LAM, AIType, Level, AIName, ...) LAM(AIType, Level, AIName) _CONCRETE_AI_FACTORY_BODY_3(LAM, AIType, Level, __VA_ARGS__)
#define _CONCRETE_AI_FACTORY_BODY_5(LAM, AIType, Level, AIName, ...) LAM(AIType, Level, AIName) _CONCRETE_AI_FACTORY_BODY_4(LAM, AIType, Level, __VA_ARGS__)

#define _GENERATE_AI_LAMBDA(AIType, Level, AIName) []() requires IsAbstractAI { return new AIName##Level; },

#define CONCRETE_AI_FACTORY(AIType, Level, ...) \
    _GET_OVERRIDE("ignored", ##__VA_ARGS__, \
    _CONCRETE_AI_FACTORY_BODY_5, _CONCRETE_AI_FACTORY_BODY_4, \
    _CONCRETE_AI_FACTORY_BODY_3, _CONCRETE_AI_FACTORY_BODY_2, \
    _CONCRETE_AI_FACTORY_BODY_1, _CONCRETE_AI_FACTORY_BODY_0) \
    (_GENERATE_AI_LAMBDA, AIType, Level, ##__VA_ARGS__)

泛型宏的实现复杂是针对开发者来说的,对于使用者来说却是极为简单。

现在你可以非常简单地使用宏实现的「具体工厂」来替换前面的写法:

template<class T>
static constexpr auto AIFactory = AbstractAIFactory {

    // []() requires IsAbstractAI { return new LuxEasy; },
    // []() requires IsAbstractAI { return new ZiggsEasy; },
    // []() requires IsAbstractAI { return new TeemoEasy; }
    CONCRETE_AI_FACTORY(T, Easy, Lux, Ziggs, Teemo)
};

不论你有多少产品,都可以由该具体工厂轻松实现,是不是很强大!


7

总结

本篇内容应该是我写的涉及内容最广的文章之一了,光之前写过的文章就引用了多篇

所以对大家的要求也会有点高,可以多看几遍。

另外这篇的内容其实很“新”,首先组合使用了许多C++20特性,其次涉及了大量泛型编程技术,文章介绍的泛型Lambda技术现在还不是很流行,使用场景也是慢慢摸索出来的,比较成熟的想法都写在了本文之中。

但是它的用处我感觉还有很多,还在研究中,后续再和大家分享。

C语言与CPP编程 C语言/C++开发,C语言/C++基础知识,C语言/C++学习路线,C语言/C++进阶,数据结构;算法;python;计算机基础等
评论
  •  光伏及击穿,都可视之为 复合的逆过程,但是,复合、光伏与击穿,不单是进程的方向相反,偏置状态也不一样,复合的工况,是正偏,光伏是零偏,击穿与漂移则是反偏,光伏的能源是外来的,而击穿消耗的是结区自身和电源的能量,漂移的载流子是 客席载流子,须借外延层才能引入,客席载流子 不受反偏PN结的空乏区阻碍,能漂不能漂,只取决于反偏PN结是否处于外延层的「射程」范围,而穿通的成因,则是因耗尽层的过度扩张,致使跟 端子、外延层或其他空乏区 碰触,当耗尽层融通,耐压 (反向阻断能力) 即告彻底丧失,
    MrCU204 2025-01-17 11:30 210浏览
  • 嘿,咱来聊聊RISC-V MCU技术哈。 这RISC-V MCU技术呢,简单来说就是基于一个叫RISC-V的指令集架构做出的微控制器技术。RISC-V这个啊,2010年的时候,是加州大学伯克利分校的研究团队弄出来的,目的就是想搞个新的、开放的指令集架构,能跟上现代计算的需要。到了2015年,专门成立了个RISC-V基金会,让这个架构更标准,也更好地推广开了。这几年啊,这个RISC-V的生态系统发展得可快了,好多公司和机构都加入了RISC-V International,还推出了不少RISC-V
    丙丁先生 2025-01-21 12:10 586浏览
  • 数字隔离芯片是一种实现电气隔离功能的集成电路,在工业自动化、汽车电子、光伏储能与电力通信等领域的电气系统中发挥着至关重要的作用。其不仅可令高、低压系统之间相互独立,提高低压系统的抗干扰能力,同时还可确保高、低压系统之间的安全交互,使系统稳定工作,并避免操作者遭受来自高压系统的电击伤害。典型数字隔离芯片的简化原理图值得一提的是,数字隔离芯片历经多年发展,其应用范围已十分广泛,凡涉及到在高、低压系统之间进行信号传输的场景中基本都需要应用到此种芯片。那么,电气工程师在进行电路设计时到底该如何评估选择一
    华普微HOPERF 2025-01-20 16:50 122浏览
  • 临近春节,各方社交及应酬也变得多起来了,甚至一月份就排满了各式约见。有的是关系好的专业朋友的周末“恳谈会”,基本是关于2025年经济预判的话题,以及如何稳定工作等话题;但更多的预约是来自几个客户老板及副总裁们的见面,他们为今年的经济预判与企业发展焦虑而来。在聊天过程中,我发现今年的聊天有个很有意思的“点”,挺多人尤其关心我到底是怎么成长成现在的多领域风格的,还能掌握一些经济趋势的分析能力,到底学过哪些专业、在企业管过哪些具体事情?单单就这个一个月内,我就重复了数次“为什么”,再辅以我上次写的:《
    牛言喵语 2025-01-22 17:10 175浏览
  • 现在为止,我们已经完成了Purple Pi OH主板的串口调试和部分配件的连接,接下来,让我们趁热打铁,完成剩余配件的连接!注:配件连接前请断开主板所有供电,避免敏感电路损坏!1.1 耳机接口主板有一路OTMP 标准四节耳机座J6,具备进行音频输出及录音功能,接入耳机后声音将优先从耳机输出,如下图所示:1.21.2 相机接口MIPI CSI 接口如上图所示,支持OV5648 和OV8858 摄像头模组。接入摄像头模组后,使用系统相机软件打开相机拍照和录像,如下图所示:1.3 以太网接口主板有一路
    Industio_触觉智能 2025-01-20 11:04 194浏览
  • 故障现象 一辆2007款日产天籁车,搭载VQ23发动机(气缸编号如图1所示,点火顺序为1-2-3-4-5-6),累计行驶里程约为21万km。车主反映,该车起步加速时偶尔抖动,且行驶中加速无力。 图1 VQ23发动机的气缸编号 故障诊断接车后试车,发动机怠速运转平稳,但只要换挡起步,稍微踩下一点加速踏板,就能感觉到车身明显抖动。用故障检测仪检测,发动机控制模块(ECM)无故障代码存储,且无失火数据流。用虹科Pico汽车示波器测量气缸1点火信号(COP点火信号)和曲轴位置传感器信
    虹科Pico汽车示波器 2025-01-23 10:46 70浏览
  • 2024年是很平淡的一年,能保住饭碗就是万幸了,公司业绩不好,跳槽又不敢跳,还有一个原因就是老板对我们这些员工还是很好的,碍于人情也不能在公司困难时去雪上加霜。在工作其间遇到的大问题没有,小问题还是有不少,这里就举一两个来说一下。第一个就是,先看下下面的这个封装,你能猜出它的引脚间距是多少吗?这种排线座比较常规的是0.6mm间距(即排线是0.3mm间距)的,而这个规格也是我们用得最多的,所以我们按惯性思维来看的话,就会认为这个座子就是0.6mm间距的,这样往往就不会去细看规格书了,所以这次的运气
    wuliangu 2025-01-21 00:15 320浏览
  •  万万没想到!科幻电影中的人形机器人,正在一步步走进我们人类的日常生活中来了。1月17日,乐聚将第100台全尺寸人形机器人交付北汽越野车,再次吹响了人形机器人疯狂进厂打工的号角。无独有尔,银河通用机器人作为一家成立不到两年时间的创业公司,在短短一年多时间内推出革命性的第一代产品Galbot G1,这是一款轮式、双臂、身体可折叠的人形机器人,得到了美团战投、经纬创投、IDG资本等众多投资方的认可。作为一家成立仅仅只有两年多时间的企业,智元机器人也把机器人从梦想带进了现实。2024年8月1
    刘旷 2025-01-21 11:15 658浏览
  • 高速先生成员--黄刚这不马上就要过年了嘛,高速先生就不打算给大家上难度了,整一篇简单但很实用的文章给大伙瞧瞧好了。相信这个标题一出来,尤其对于PCB设计工程师来说,心就立马凉了半截。他们辛辛苦苦进行PCB的过孔设计,高速先生居然说设计多大的过孔他们不关心!另外估计这时候就跳出很多“挑刺”的粉丝了哈,因为翻看很多以往的文章,高速先生都表达了过孔孔径对高速性能的影响是很大的哦!咋滴,今天居然说孔径不关心了?别,别急哈,听高速先生在这篇文章中娓娓道来。首先还是要对各位设计工程师的设计表示肯定,毕竟像我
    一博科技 2025-01-21 16:17 158浏览
  • Ubuntu20.04默认情况下为root账号自动登录,本文介绍如何取消root账号自动登录,改为通过输入账号密码登录,使用触觉智能EVB3568鸿蒙开发板演示,搭载瑞芯微RK3568,四核A55处理器,主频2.0Ghz,1T算力NPU;支持OpenHarmony5.0及Linux、Android等操作系统,接口丰富,开发评估快人一步!添加新账号1、使用adduser命令来添加新用户,用户名以industio为例,系统会提示设置密码以及其他信息,您可以根据需要填写或跳过,命令如下:root@id
    Industio_触觉智能 2025-01-17 14:14 145浏览
  •     IPC-2581是基于ODB++标准、结合PCB行业特点而指定的PCB加工文件规范。    IPC-2581旨在替代CAM350格式,成为PCB加工行业的新的工业规范。    有一些免费软件,可以查看(不可修改)IPC-2581数据文件。这些软件典型用途是工艺校核。    1. Vu2581        出品:Downstream     
    电子知识打边炉 2025-01-22 11:12 134浏览
  • 本文介绍瑞芯微开发板/主板Android配置APK默认开启性能模式方法,开启性能模式后,APK的CPU使用优先级会有所提高。触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。源码修改修改源码根目录下文件device/rockchip/rk3562/package_performance.xml并添加以下内容,注意"+"号为添加内容,"com.tencent.mm"为AP
    Industio_触觉智能 2025-01-17 14:09 203浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦