[Rust笔记]浅聊泛型常量参数 Const Generic

浅聊泛型常量参数Const Generic

引题

最近有网友私信我讨论:若使用规则宏编译时统计token tree序列的长度,如何绕开由宏递归自身局限性造成的:

  • 被统计序列不能太长

  • 编译延时显著拖长

的问题。然后,就贴出了如下的一段例程代码1:

fn main() {macro_rules! count_tts {($_a:tt $($tail: tt)*) => { 1_usize + count_tts!($($tail)*) };() => { 0_usize };}assert_eq!(10, count_tts!(,,,,,,,,,,));
}

嚯!这段短小精悍的代码馁馁地演示了Incremental TT Muncher设计模式的精髓。赞!

首先,宏递归深度是有极限的(默认是128层)。所以,若每次递归仅新统计一个token,那么被统计序列的最大长度自然不能超过128。否则,突破上限,编译失败!

其次,尾递归优化运行时压缩函数调用栈的技术手段,却做不到编译时抑制调用栈的膨胀。所以,巧用#![recursion_limit="…"]元属性强制调高宏递归深度上限很可能会导致编译器栈溢出。

由此,如果仅追求快速绕过问题,那最经济实惠的作法是:在每次宏递归期间,统计几个token 例程2(而不是一次一个)。从算数上,将总递归次数降下来,和使计数更长的token tree序列成为可能。

fn main() { // 这代码看着就“傻乎乎的”。macro_rules! count_tts {($_a: tt $_b: tt $_c: tt $_d: tt $_e: tt $_f: tt // 一次递归统计 6 个。$($tail: tt)*) => { 6_usize + count_tts!($($tail)*) };($_a: tt $_b: tt $_c: // 一次递归统计 3 个。tt $($tail: tt)*) => { 3_usize + count_tts!($($tail)*) };($_a: tt // 一次递归统计 1 个。$($tail: tt)*) => { 1_usize + count_tts!($($tail)*) };() => { 0_usize };  // 结束了,统计完成}println!("token tree 个数是 {}", count_tts!(,,,,,,,,,,));
}

倘若要标本兼治地解决问题,将递归调用变形成循环结构才是正途,因为循环本身不会增加调用栈的深度。这涵盖了:

  1. 宏循环结构token tree序列变形成数组字面量

  2. 常量函数调用触发编译器对数组字面量的类型推导

  3. 因为rust数组在编译时明确大小,所以数组长度被编入了数据类型定义内。

  4. 泛型常量参数从数据类型定义中提取出数组长度值,并作为序列长度返回。

全套操作被统称为Array length设计模式。它带入了两个技术难点:

  1. 如何触发rustc对数组字面量的类型推导,和从推导结果中提取出数组长度信息。

  2. 如何撇开递归的“吐吞模式”(即,吐Incremental TT Muncher和吞Push-down Accumulation),仅凭宏循环结构,将token tree序列变形成为数组字面量。

第一个难点源于自rustc 1.51才稳定的新语言特性“泛型常量参数Const Generic”。而第二个难点的解决就多样化了

  • 要么,采用“循环替换设计模式Repetition Replacement(RR)

  • 要么,启用试验阶段语言特性“元变量表达式Meta-variable Expression

接下来,它们会被逐一地讲解分析。

泛型常量参数

rustc 1.51+起,【泛型常量参数 】允许泛型项(类或函数)接受常量值或常量表达式为泛型参数。根据泛型常量参数出现的位置不同(请见下图例程3),它又细分为

  • 泛型常量参数的

  • 泛型常量参数的

58196eed220b4f0175e0478d3e5a1b18.png

下文分别将它们简称为“泛型常量形参”与“泛型常量实参”。

泛型参数的分类

于是,已知的泛型参数就包含有三种类型:

a87222dfbd37963f56692291d55223ea.png

泛型常量参数的数据类型

可用作【泛型常量参数】的数据类型包括两类:

  • 整数数字类型:u8u16u32u64u128usizei8i16i32i64i128isize

  • 数字化类型:charbool

泛型常量参数的“怪癖”

首先,就“同名冲突”而言,若【泛型常量形参】与【类型】同名并作为另一个泛型项的泛型参数实参,那么rustc会优先将该泛型参数当作类型带入程序上下文。多数情况下,这会造成程序编译失败。解决方案是使用表达式{...}包装泛型常量参数,以向rustc标注此同名参数是泛型常量参数而不是类型名 例程4。

bd1bd876f750981a0c1c71341852f150.png

其次,就“声明和使用”而言,泛型常量参允许仅被声明,而不被使用。对另两种泛型参数而言,这却会导致编译失败例程5。

649bcf28fb78277760799640bd030f48.png

最后,泛型常量参的trait实现不会因为穷举了全部备选形参值而自动过渡给泛型常量参。如下例程6(左),即便泛型项struct Foo显示地给泛型常量B每个可能的(参)值true / false都实现的同一个trait Bar,编译器也不会“聪明地”归纳出该trait Bar已经被此泛型项的泛型常量参充分实现了,因为编译器可不会“归纳法”方法论(不确定chatGPT是否能做到?)。相反,每个参上的trait实现都被视作不相关的个例。正确地作法是:泛型项必须明确地给泛型常量参实现trait例程7(右)。

03b0eec1bd0d5451e34ad855568ffa05.png

泛型常量参数的适用位置

泛型常量参数原则上可出现于常量项适用的全部位置,包括但不限于:

  • 运行时求值表达式 #1 — 模糊了编译时泛型参数与运行时值之间的界限。

  • 常量表达式 #2

  • 关联常量 #2

  • 关联类型 #3

  • 结构体字段 或 绑定变量的数据类型 #4。比如,编译时参数化数组长度。

  • 结构体字段 或 绑定变量的值 #5

上述列表内的#1 ~ #5,可在下面例程8源码内找到对应的代码行。

use rand::{thread_rng, Rng};
fn main() {fn foo1<const N1: usize>(input: usize) { // 在泛型函数内,泛型常量参数的形参可用于let sum = 1 + N1 * input;   // #1 运行时求值的表达式let foo = Foo([input; N1]); // #5 结构体字段的值let arr: [usize; N1] = [input; N1]; // #4 绑定变量的数据类型 —— 编译时参数化数组长度// #5 绑定变量的值println!("运行时表达式:{sum},\n\元组结构体:  {foo:?},\n\数组:       {arr:?}");}trait Trait<const N2: usize> {const CONST: usize = N2 + 4; // #2 关联常量 + 常量表达式type Output;}#[derive(Debug)]struct Foo<const N3: usize>([usize; N3] // #4 结构体字段的数据类型 —— 编译时参数化数组长度);impl<const N4: usize> Trait<N4> for Foo<N4> {type Output = [usize; N4]; // #3 关联类型 —— 编译时参数化数组长度}let mut rng = thread_rng();foo1::<2>(rng.gen_range::<usize, _>(1..10));foo1::<{1 + 2}>(rng.gen_range::<usize, _>(1..10));const K: usize = 3;foo1::<K>(rng.gen_range::<usize, _>(1..10));foo1::<{K * 2}>(rng.gen_range::<usize, _>(1..10));
}

泛型常量参数的不适用位置

首先,泛型常量参不能:

  • 定义常量静态变量,无论是作为类型定义的一部分,还是值 #1

  • 隔层使用。比如,在子函数内引用由外层函数声明的泛型常量#2。除了子函数,该规则也适用于在函数体内定义的

    • 结构体 #3

    • 类型别名 #4

上述列表内的#1 ~ #4,可在下面例程9源码内找到对应的代码行。

fn main() {fn outer<const N: usize>(input: usize) {// 泛型常量参数【不】可用于函数体内的// #1 常量定义//     - 既不能定义类型const BAD_CONST: [usize; N] = [1; N];//     - 既不能定义值const BAD_CONST: usize = 1 + N;// #1 静态变量定义//     - 既不能定义类型static BAD_STATIC: [usize; N] = [N + 1; N];//     - 既不能定义值static BAD_STATIC: usize = 1 + N;fn inner(bad_arg: [usize; N]) {// #2 在子函数内不能引用外层函数声明的//    泛型常量形参,无论是将其作为//    变量类型,还是常量值。let bad_value = N * 2;}// #3 结构体内也不能引用外层函数声明的//    泛型常量形参。struct BadStruct([usize; N]);//    相反,需要给结构体重新声明泛型常量参数struct BadStruct<const N: usize>([usize; N]);// #4 类型别名内不能引用外层函数声明的//    泛型常量形参。type BadAlias = [usize; N];//    相反,需要给类型别名重新声明泛型常量参数type BadAlias<const N: usize> = [usize; N];}
}

其次,泛型常量接受包含了泛型常量参的常量表达式例程10。

5053469e32df355eb46ac5b3a3869b91.png

但是,泛型常量参并不拒绝接受

  • 独立泛型常量参 例程11

  • 包含泛型常量参的普通常量表达式例程12

    题外话,不确定这么翻译该术语lookahead是否正确。我借鉴了 @余晟 在《精通正则表达式》一书中对此词条的译文。

    • 被用作泛型常量参的常量表达式必须被包装在表达式{...}内。避免编译器在解析AST过程中陷入正向环视lookahead的无限循环中。

数组重复表达式与泛型常量参数

数组重复表达式[repeat_operand; length_operand]是数组字面量的一种形式。在数组重复表达式中,泛型常量形参

  • 虽然既可用于左repeat操作数位置,也可用于右length操作数位置例程13

  • 但在右length操作数位置上,泛型常量参只能独立出现例程14,而不能作为常量表达式的一部分 —— 等同于泛型常量参的限制。

6d9c415204b4b45dd3ebc3d1ba03b109.png

回到序列计数问题

类似于解析几何中的“投影”方法,通过将高维物体(token tree序列)投影于低维平面(数组),以主动舍弃若干信息项(每个token的具体值与数据类型)为代价,突出该物体更有价值的信息内容(序列长度),便可降低从复杂结构中摘取特定关注信息项的合计复杂度。这套“降维算法”带来的启发就是:

  1. 既然读取数组长度是简单的,那为什么不先将token tree序列变形为数组呢?

    1. 答:投影token tree序列为数组

  2. 既然token tree序列的内容细节不被关注,那为什么还要纠结于数组的数据类型与填充值呢?全部充满unit type岂不快哉!

    1. 再答:投影token tree序列为单位数组[(); N]。仅数组长度对我们有价值。

于是,循环替换设计模式Repetition Replacement(RR)与元变量表达式${ignore(识别符名)}都是被用来改善【宏循环结构】的使用体验,以允许Rustacean对循环结构中的循环重复项“宣而不用” —— 既遍历token tree序列,同时又弃掉每个具体的token元素,最后还生成一个等长的单位数组[(); N]。否则,未被使用的“循环重复项”会导致error: attempted to repeat an expression containing no syntax variables matched as repeating at this depth的编译错误。

  • 循环替换设计模式Repetition Replacement(RR)是以在宏循环体内插入一层“空转”宏调用,消费掉consuming未被使用的“循环重复项”例程15

  • 元变量表达式${ignore(识别符名)}是前者的语法糖,允许Rustacean少敲几行代码。但因为元变量表达式是试验性的新语法,所以需要开启对应的feature-gate开关#![feature(macro_metavar_expr)]才能被使用。例程16

然后,常量函数调用和函数参触发编译器对单位数组字面量的类型推导

接着,泛型常量参从被推导出的数据类型定义内提取出数组长度信息。

最后,将泛型常量参作为常量函数的返回值输出。

上图,一图抵千词。

33cd0210f15dd70b5f8e0db09e033b59.png

结束语

除了前文提及的【宏递归法】与Array Length设计模式,统计token tree序列长度还有

  • Slice Length设计模式

    • 原理类似Array Length,但调用数组字面量的pub const fn len(&self) -> usize成员方法读取长度值(而不是依赖类型推导和泛型参数提取)。

  • 枚举计数法

    • 规则宏将token tree序列变形为“枚举类”(而不是数组字面量),再由最后一个枚举值的分辨因子discriminant值加1获得序列长度。

    • 但,缺点也明显。比如,token tree序列内不能包含rust语法关键字与重复项。

  • 比特计数法

    • 典型的算法优化。从数学层面,将程序复杂度从O(n)降到O(log(n))。有些复杂,回头单独写一篇文章分享之。

【规则宏】与【泛型参数】皆是rust编程语言提供的业务功能开发利器。宏循环结构与泛型常量参数仅只是它们的冰山一角。此文既汇总分享与网友的讨论成果,也对此话题抛砖引玉。希望有机会与路过的神仙哥哥和仙女妹妹们更深入地交流相关技术知识点与实践经验。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/25955.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

我为AI艺术开发了一个专有的作品展示、销售、交流平台 —— KALOS.art

从 4 月份开始沉迷 AI 图像生成之后&#xff0c;“作品量” 和 创作热情 同步上涨。每周我都能跑出几十上百张自己看着很像那么回事的作品&#xff08;至少在当时很满意&#xff09;。有了作品就想秀&#xff0c;这是压抑不住的本能冲动。于是我就在一切能秀图的地方发&#xf…

微软为何总能重返浪潮之巅?

文 | 孙静 01 「牛得不像微软」 2010年3月3日&#xff0c;微软前CEO鲍尔默在媒体面前强撑自信。他说&#xff0c;「终有一天&#xff0c;必应将超越谷歌。」 这个许诺被加了一个模糊的期限&#xff1a;数年&#xff0c;或十几年之内。当时谷歌在美国搜索市场份额高达65%&#…

最新AI创作系统源码V5.0.9+支持GPT4.0+支持ai绘画+实时语音识别输入

最新AI创作系统源码V5.0.9支持GPT4.0支持ai绘画实时语音识别输入 一、AI创作系统二、系统介绍三、系统程序下载四、安装教程五、主要功能展示六、更新日志 一、AI创作系统 1、提问&#xff1a;程序已经支持GPT3.5、GPT4.0接口、支持新建会话&#xff0c;上下文记忆 2、支持三种…

实测GPT-4,不到1小时写完了一个小程序界面,推理能力提升能直接破译密文?

文章目录 GPT4生成小程序 GPT4文字总结能力 GPT4推理提升与密码破译 评论留言 我想问下审核人员是哪里来的广告&#xff1f;&#xff1f;&#xff1f; 开始 最近GPT4仅靠一张草稿生成一个网页的图片被疯传。心动之下我决定升级chatGPT尝鲜&#xff0c;试试用一张草稿生成一…

AI创作系统ChatGPT网站源码+新增GPT联网功能+支持GPT4+支持ai绘画+实时语音识别输入

AI创作系统ChatGPT网站源码新增GPT联网功能支持GPT4支持ai绘画实时语音识别输入 一、AI创作系统二、系统介绍三、系统程序下载四、安装教程五、其他主要功能展示六、更新日志 一、AI创作系统 提问&#xff1a;程序已经支持GPT3.5、GPT4.0接口、支持新建会话&#xff0c;上下文…

【无标题】练字

一点点积累 一点点努力 一定能行&#xff01;为了十级&#xff01;努力奋斗&#xff01;

给成年朋友练字的建议

老话说“字是一个人的门面”&#xff0c;虽然工作之后手写的场合少了&#xff0c;但想装修这一门面的成年人可不少呢&#xff0c;不信&#xff0c;看看知乎上的热门话题&#xff1a;“成年人怎样练字效果最好&#xff1f;”&#xff0c;“成年人练字有哪些高效率的方法?”&…

练字和平时写字完全不一样怎么办?

amy &#xff0c;书法&#xff5c;飙车&#xff5c;文化民工 245 人赞同 于2015.1.30更新 首先 感谢各位知友支持。那我就再补充些内容。 第一&#xff1a;练字的方法其实很简单&#xff0c;我已经很详尽了&#xff0c;不要想太难。大体内容我不打算变动&#xff0c;对于第一次…

想练字要怎么选择字体?

adios &#xff0c;很有可能是个帅哥 98 人赞同 看题主的字&#xff0c;实话说结构杂乱&#xff0c;笔画随意&#xff0c;基础差。 所以先推荐田英章老师的楷书&#xff0c;不要觉得俗&#xff0c;田老师是学欧的&#xff0c;架构相当了得。 先上欧楷。 卢中南小楷《唐诗三百首…

盲打训练【练字】

盲打训练【练字】 2022.11.20 1822021.09.02 1042021.08.30 1012021.08.29 882021.08.28 842021.08.26 662021.08.25 67 2022.11.20 182 2021.09.02 104 2021.08.30 101 2021.08.29 88 2021.08.28 84 2021.08.26 66 中午 2021.08.25 67

JavaScript练字游戏

最近手痒&#xff0c;把一个有三种难度的练习打字得分的小游戏做了出来…css可以自己添加。 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, i…

练字一定要用钢笔吗?

松坂楠晗 &#xff0c;励志成为全世界最可爱的女孩子&#xff01; 137 人赞同 练字一定要用钢笔么&#xff1f;钢笔字练好了用圆珠笔墨水笔写字也会受益么&#xff1f; 舞蹈一定要学芭蕾么&#xff1f;芭蕾学好了跳爵士街舞也会受益么&#xff1f; 乐器一定要学钢琴么&#xff…

练字的诀窍有哪些?

Chilly &#xff0c;我不过是喜欢写字而已 陈柏龄、Naiyan Wang 等 7118 人赞同 对所有想把字写好的人来说&#xff0c;练字没有诀窍。但有方法。 两大步&#xff1a; 1、认知。了解什么是好字&#xff0c;写好汉字的关键是什么。 2、实践。写好字的决心&#xff0c;经常练习与…

英文如何练字?

凡心 &#xff0c;我想看见星辰大海 2265 人赞同 今日最后一答。 这是我从小学开始直到2014年5月6日以前的英文字体。 这是我2014年5月底的字体 这是我现在的字体。 OK。不多说&#xff0c;直接开始正文。 阶段一&#xff1a;意大利体 适用对象&#xff1a;所有考生、实用主义者…

练字心得

对所有想把字写好的人来说&#xff0c;练字没有诀窍。但有方法。 两大步&#xff1a; 1、认知。了解什么是好字&#xff0c;写好汉字的关键是什么。 2、实践。写好字的决心&#xff0c;经常练习与经常观摩的习惯。 1、认知。了解什么是好字&#xff0c;写好汉字的关键是什么…

我的练字记录

——喂&#xff01;这里是CSDN啊&#xff0c;你在这里练字算什么&#xff1f; ——程序员不写字的么&#xff1f; ——嗯&#xff0c;有道理。。。。 &#xff08;本文以倒序方式记录&#xff0c;请从结尾处开始阅读&#xff09; ❤ 2020.5.5 ❤ 练字之前&#xff0c;我在b站上…

打字测试软件 tt,打字测试(TT)

这是打字测试(TT)&#xff0c;是LCX软件工作室制作的&#xff0c;供练习、测试中英文打字的朋友使用。经过多次试用&#xff0c;效果很好。特别适合很多学生在局域网中进行中英文的打字测试&#xff0c;是计算机教师的好帮手。 软件介绍 1、新版本突破了原来的页面形式&#xf…

如何使用中国知网查询文献,并自动生成参考文献格式引文?

如何使用中国知网查询文献? 一、登录打开中国知网校内登录校外 二、检索并下载文献1.输入检索关键字2.选择我们需要下载的文献3.自动生成参考文献格式引文 致谢 一、登录打开中国知网 校内登录 1.百度搜索中国知网&#xff0c;或者点击中国知网链接跳转得到如下页面。 2.点…

教你如何将中国知网下载的caj文献转成pdf文献

教你如何将中国知网下载的CAJ文献转成PDF文献 CAJ文件如下图&#xff1a; 第一步&#xff1a;装虚拟打印机&#xff1a; 从搜索引擎上下载虚拟打印机&#xff08;随便&#xff09;并安装。 第二步&#xff1a;装CAJViewer&#xff1a; 从中国知网上下载CAJViewer并安装&#xf…

知网导出bib文件(无需第三方工具,保姆级)

起因 最近一门课要交一篇论文&#xff0c;在知网上导出参考文献格式时发现没有BibTex格式&#xff0c; 于是打开搜索引擎&#xff0c;发现大都需要安装如EndNotes的第三方软件&#xff0c;个人不喜欢一遇到问题就去下第三方工具&#xff0c;以免开辟新坑。于是自己琢磨了一番&a…