淺聊泛型常量參數(shù)Const Generic
引題
最近有網(wǎng)友私信我討論:若使用規(guī)則宏編譯時統(tǒng)計token tree
序列的長度,如何繞開由宏遞歸自身局限性造成的:
- 被統(tǒng)計序列不能太長
- 編譯延時顯著拖長
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
設(shè)計模式的精髓。贊!
首先,宏遞歸深度是有極限的(默認(rèn)是128
層)。所以,若每次遞歸僅新統(tǒng)計一個token
,那么被統(tǒng)計序列的最大長度自然不能超過128
。否則,突破上限,編譯失??!
其次,尾遞歸優(yōu)化是運行時壓縮函數(shù)調(diào)用棧的技術(shù)手段,卻做不到編譯時抑制宏調(diào)用棧的膨脹。所以,巧用#![recursion_limit="…"]
元屬性強制調(diào)高宏遞歸深度上限很可能會導(dǎo)致編譯器棧溢出。
由此,如果僅追求快速繞過問題,那最經(jīng)濟實惠的作法是:在每次宏遞歸期間,多統(tǒng)計幾個token
例程2(而不是一次一個)。從算數(shù)上,將總遞歸次數(shù)降下來,和使計數(shù)更長的token tree
序列成為可能。fn main() { // 這代碼看著就“傻乎乎的”。 macro_rules! count_tts { ($_a: tt $_b: tt $_c: tt $_d: tt $_e: tt $_f: tt // 一次遞歸統(tǒng)計 6 個。 $($tail: tt)*) => { 6_usize + count_tts!($($tail)*) }; ($_a: tt $_b: tt $_c: // 一次遞歸統(tǒng)計 3 個。 tt $($tail: tt)*) => { 3_usize + count_tts!($($tail)*) }; ($_a: tt // 一次遞歸統(tǒng)計 1 個。 $($tail: tt)*) => { 1_usize + count_tts!($($tail)*) }; () => { 0_usize }; // 結(jié)束了,統(tǒng)計完成 } println!("token tree 個數(shù)是 {}", count_tts!(,,,,,,,,,,)); } 倘若要標(biāo)本兼治地解決問題,將遞歸調(diào)用變形成循環(huán)結(jié)構(gòu)才是正途,因為循環(huán)本身不會增加調(diào)用棧的深度。這涵蓋了:
-
宏循環(huán)結(jié)構(gòu)將
token tree
序列變形成數(shù)組字面量。 - 常量函數(shù)調(diào)用觸發(fā)編譯器對數(shù)組字面量的類型推導(dǎo)。
-
因為
rust
數(shù)組在編譯時明確大小,所以數(shù)組長度被編入了數(shù)據(jù)類型定義內(nèi)。 - 泛型常量參數(shù)從數(shù)據(jù)類型定義中提取出數(shù)組長度值,并作為序列長度返回。
Array length
設(shè)計模式。它帶入了兩個技術(shù)難點:-
如何觸發(fā)
rustc
對數(shù)組字面量的類型推導(dǎo),和從推導(dǎo)結(jié)果中提取出數(shù)組長度信息。 -
如何撇開遞歸的“吐吞模式”(即,吐
Incremental TT Muncher
和吞Push-down Accumulation
),僅憑宏循環(huán)結(jié)構(gòu),將token tree
序列變形成為數(shù)組字面量。
rustc 1.51
才穩(wěn)定的新語言特性“泛型常量參數(shù)Const Generic
”。而第二個難點的解決就多樣化了-
要么,采用“循環(huán)替換設(shè)計模式
Repetition Replacement(RR)
” -
要么,啟用試驗階段語言特性“元變量表達式
Meta-variable Expression
”
泛型常量參數(shù)
從rustc 1.51+
起,【泛型常量參數(shù) 】允許泛型項(類或函數(shù))接受常量值或常量表達式為泛型參數(shù)。根據(jù)泛型常量參數(shù)出現(xiàn)的位置不同(請見下圖例程3),它又細(xì)分為- 泛型常量參數(shù)的形參
- 泛型常量參數(shù)的實參
下文分別將它們簡稱為“泛型常量形參”與“泛型常量實參”。
泛型參數(shù)的分類
于是,已知的泛型參數(shù)就包含有三種類型:泛型常量參數(shù)的數(shù)據(jù)類型
可用作【泛型常量參數(shù)】的數(shù)據(jù)類型包括兩類:-
整數(shù)數(shù)字類型:
u8
,u16
,u32
,u64
,u128
,usize
,i8
,i16
,i32
,i64
,i128
,isize
-
可數(shù)字化類型:
char
,bool
泛型常量參數(shù)的“怪癖”
首先,就“同名沖突”而言,若【泛型常量形參】與【類型】同名并作為另一個泛型項的泛型參數(shù)實參,那么rustc
會優(yōu)先將該泛型參數(shù)當(dāng)作類型帶入程序上下文。多數(shù)情況下,這會造成程序編譯失敗。解決方案是使用塊表達式{...}
包裝泛型常量參數(shù),以向rustc
標(biāo)注此同名參數(shù)是泛型常量參數(shù)而不是類型名例程4。其次,就“聲明和使用”而言,泛型常量形參允許僅被聲明,而不被使用。對另兩種泛型參數(shù)而言,這卻會導(dǎo)致編譯失敗例程5。 最后,泛型常量實參的
trait
實現(xiàn)不會因為窮舉了全部備選形參值而自動過渡給泛型常量形參。如下例程6(左),即便泛型項struct Foo
顯示地給泛型常量形參B
的每個可能的(實參)值true / false
都實現(xiàn)的同一個trait Bar
,編譯器也不會“聰明地”歸納出該trait Bar
已經(jīng)被此泛型項的泛型常量形參充分實現(xiàn)了,因為編譯器可不會“歸納法”方法論(不確定chatGPT
是否能做到?)。相反,每個實參上的trait
實現(xiàn)都被視作不相關(guān)的個例。正確地作法是:泛型項必須明確地給泛型常量形參實現(xiàn)trait
例程7(右)。
泛型常量參數(shù)的適用位置
泛型常量參數(shù)原則上可出現(xiàn)于常量項適用的全部位置,包括但不限于:-
運行時求值表達式
#1
— 模糊了編譯時泛型參數(shù)與運行時值之間的界限。 -
常量表達式
#2
-
關(guān)聯(lián)常量
#2
-
關(guān)聯(lián)類型
#3
-
結(jié)構(gòu)體字段 或 綁定變量的數(shù)據(jù)類型
#4
。比如,編譯時參數(shù)化數(shù)組長度。 -
結(jié)構(gòu)體字段 或 綁定變量的值
#5
#1 ~ #5
,可在下面例程8源碼內(nèi)找到對應(yīng)的代碼行。use rand::{thread_rng, Rng}; fn main() { fn foo1<const N1: usize>(input: usize) { // 在泛型函數(shù)內(nèi),泛型常量參數(shù)的形參可用于 let sum = 1 + N1 * input; // #1 運行時求值的表達式 let foo = Foo([input; N1]); // #5 結(jié)構(gòu)體字段的值 let arr: [usize; N1] = [input; N1]; // #4 綁定變量的數(shù)據(jù)類型 —— 編譯時參數(shù)化數(shù)組長度 // #5 綁定變量的值 println!("運行時表達式:{sum}, 元組結(jié)構(gòu)體: {foo:?}, 數(shù)組: {arr:?}"); } trait Trait<const N2: usize> { const CONST: usize = N2 + 4; // #2 關(guān)聯(lián)常量 + 常量表達式 type Output; } #[derive(Debug)] struct Foo<const N3: usize>( [usize; N3] // #4 結(jié)構(gòu)體字段的數(shù)據(jù)類型 —— 編譯時參數(shù)化數(shù)組長度 ); impl<const N4: usize> Trait
泛型常量參數(shù)的不適用位置
首先,泛型常量形參不能:-
定義常量和靜態(tài)變量,無論是作為類型定義的一部分,還是值
#1
-
隔層使用。比如,在子函數(shù)內(nèi)引用由外層函數(shù)聲明的泛型常量形參
#2
。除了子函數(shù),該規(guī)則也適用于在函數(shù)體內(nèi)定義的-
結(jié)構(gòu)體
#3
-
類型別名
#4
-
結(jié)構(gòu)體
#1 ~ #4
,可在下面例程9源碼內(nèi)找到對應(yīng)的代碼行。fn main() { fn outer<const N: usize>(input: usize) { // 泛型常量參數(shù)【不】可用于函數(shù)體內(nèi)的 // #1 常量定義 // - 既不能定義類型 const BAD_CONST: [usize; N] = [1; N]; // - 既不能定義值 const BAD_CONST: usize = 1 + N; // #1 靜態(tài)變量定義 // - 既不能定義類型 static BAD_STATIC: [usize; N] = [N + 1; N]; // - 既不能定義值 static BAD_STATIC: usize = 1 + N; fn inner(bad_arg: [usize; N]) { // #2 在子函數(shù)內(nèi)不能引用外層函數(shù)聲明的 // 泛型常量形參,無論是將其作為 // 變量類型,還是常量值。 let bad_value = N * 2; } // #3 結(jié)構(gòu)體內(nèi)也不能引用外層函數(shù)聲明的 // 泛型常量形參。 struct BadStruct([usize; N]); // 相反,需要給結(jié)構(gòu)體重新聲明泛型常量參數(shù) struct BadStruct<const N: usize>([usize; N]); // #4 類型別名內(nèi)不能引用外層函數(shù)聲明的 // 泛型常量形參。 type BadAlias = [usize; N]; // 相反,需要給類型別名重新聲明泛型常量參數(shù) type BadAlias<const N: usize> = [usize; N]; } } 其次,泛型常量實參不接受包含了泛型常量形參的常量表達式例程10。
但是,泛型常量實參并不拒絕接受
- 獨立泛型常量形參例程11
-
不包含泛型常量形參的普通常量表達式例程12
題外話,不確定這么翻譯該術(shù)語
lookahead
是否正確。我借鑒了 @余晟 在《精通正則表達式》一書中對此詞條的譯文。-
被用作泛型常量實參的常量表達式必須被包裝在塊表達式
{...}
內(nèi)。避免編譯器在解析AST
過程中陷入正向環(huán)視lookahead
的無限循環(huán)中。
-
被用作泛型常量實參的常量表達式必須被包裝在塊表達式
數(shù)組重復(fù)表達式與泛型常量參數(shù)
數(shù)組重復(fù)表達式[repeat_operand; length_operand]
是數(shù)組字面量的一種形式。在數(shù)組重復(fù)表達式中,泛型常量形參-
雖然既可用于左
repeat
操作數(shù)位置,也可用于右length
操作數(shù)位置例程13 -
但在右
length
操作數(shù)位置上,泛型常量形參只能獨立出現(xiàn)例程14,而不能作為常量表達式的一部分 —— 等同于泛型常量實參的限制。
回到序列計數(shù)問題
類似于解析幾何中的“投影”方法,通過將高維物體(token tree
序列)投影于低維平面(數(shù)組),以主動舍棄若干信息項(每個token
的具體值與數(shù)據(jù)類型)為代價,突出該物體更有價值的信息內(nèi)容(序列長度),便可降低從復(fù)雜結(jié)構(gòu)中摘取特定關(guān)注信息項的合計復(fù)雜度。這套“降維算法”帶來的啟發(fā)就是:-
既然讀取數(shù)組長度是簡單的,那為什么不先將
token tree
序列變形為數(shù)組呢?-
答:投影
token tree
序列為數(shù)組
-
答:投影
-
既然
token tree
序列的內(nèi)容細(xì)節(jié)不被關(guān)注,那為什么還要糾結(jié)于數(shù)組的數(shù)據(jù)類型與填充值呢?全部充滿unit type
豈不快哉!-
再答:投影
token tree
序列為單位數(shù)組[(); N]
。僅數(shù)組長度對我們有價值。
-
再答:投影
Repetition Replacement(RR)
與元變量表達式${ignore(識別符名)}
都是被用來改善【宏循環(huán)結(jié)構(gòu)】的使用體驗,以允許Rustacean
對循環(huán)結(jié)構(gòu)中的循環(huán)重復(fù)項“宣而不用” —— 既遍歷token tree
序列,同時又棄掉每個具體的token
元素,最后還生成一個等長的單位數(shù)組[(); N]
。否則,未被使用的“循環(huán)重復(fù)項”會導(dǎo)致error: attempted to repeat an expression containing no syntax variables matched as repeating at this depth
的編譯錯誤。-
循環(huán)替換設(shè)計模式
Repetition Replacement(RR)
是以在宏循環(huán)體內(nèi)插入一層“空轉(zhuǎn)”宏調(diào)用,消費掉consuming
未被使用的“循環(huán)重復(fù)項”例程15 -
元變量表達式
${ignore(識別符名)}
是前者的語法糖,允許Rustacean
少敲幾行代碼。但因為元變量表達式是試驗性的新語法,所以需要開啟對應(yīng)的feature-gate
開關(guān)#![feature(macro_metavar_expr)]
才能被使用。例程16
結(jié)束語
除了前文提及的【宏遞歸法】與Array Length
設(shè)計模式,統(tǒng)計token tree
序列長度還有-
Slice Length
設(shè)計模式-
原理類似
Array Length
,但調(diào)用數(shù)組字面量的pub const fn len(&self) -> usize
成員方法讀取長度值(而不是依賴類型推導(dǎo)和泛型參數(shù)提取)。
-
原理類似
-
枚舉計數(shù)法
-
規(guī)則宏將
token tree
序列變形為“枚舉類”(而不是數(shù)組字面量),再由最后一個枚舉值的分辨因子discriminant
值加1
獲得序列長度。 -
但,缺點也明顯。比如,
token tree
序列內(nèi)不能包含rust
語法關(guān)鍵字與重復(fù)項。
-
規(guī)則宏將
-
比特計數(shù)法
-
典型的算法優(yōu)化。從數(shù)學(xué)層面,將程序復(fù)雜度從
O(n)
降到O(log(n))
。有些復(fù)雜,回頭單獨寫一篇文章分享之。
-
典型的算法優(yōu)化。從數(shù)學(xué)層面,將程序復(fù)雜度從
rust
編程語言提供的業(yè)務(wù)功能開發(fā)利器。宏循環(huán)結(jié)構(gòu)與泛型常量參數(shù)僅只是它們的冰山一角。此文既匯總分享與網(wǎng)友的討論成果,也對此話題拋磚引玉。希望有機會與路過的神仙哥哥和仙女妹妹們更深入地交流相關(guān)技術(shù)知識點與實踐經(jīng)驗。
審核編輯:湯梓紅
聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。
舉報投訴
-
參數(shù)
+關(guān)注
關(guān)注
11文章
1763瀏覽量
32051 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4286瀏覽量
62336 -
編譯
+關(guān)注
關(guān)注
0文章
649瀏覽量
32776 -
數(shù)據(jù)類型
+關(guān)注
關(guān)注
0文章
236瀏覽量
13596
原文標(biāo)題:淺聊泛型常量參數(shù)
文章出處:【微信號:Rust語言中文社區(qū),微信公眾號:Rust語言中文社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
詳解Rust的泛型
所有的編程語言都致力于將重復(fù)的任務(wù)簡單化,并為此提供各種各樣的工具。在 Rust 中,泛型(generics)就是這樣一種工具,它是具體類型或其它屬性的抽象替代。在編寫代碼時,我們可以直接描述泛
發(fā)表于 11-12 09:08
?1041次閱讀
Go語言常量的聲明
在 Go 語言中, 常量 表示的是固定的值,常量表達式的值在編譯期進行計算,常量的值不可以修改。例如:3 、 Let's go 、 3.14 等等。常量中的數(shù)據(jù)類型只可以是
發(fā)表于 07-20 15:24
?385次閱讀
Golang泛型的使用
眾所周知很多語言的function 中都支持 key=word 關(guān)鍵字參數(shù), 但 golang 是不支持的, 我們可以利用泛型去簡單的實現(xiàn)。
發(fā)表于 08-16 12:24
?262次閱讀
labview連接mongdb問題,找到不.NET類中的泛型類
有沒有人用labview連接mongodb數(shù)據(jù)庫的?已下載mongodb的c#驅(qū)動,利用labview中的.net控件調(diào)用相關(guān)函數(shù),但是驅(qū)動中有部分函數(shù)在泛型類中, labview能調(diào)用c#中的泛
發(fā)表于 04-08 13:38
C語言教程之?dāng)?shù)值型常量的使用
C語言教程之?dāng)?shù)值型常量的使用,很好的C語言資料,快來學(xué)習(xí)吧。
發(fā)表于 04-22 11:06
?0次下載
聊聊java泛型實現(xiàn)的原理與好處
摘要: 和C++以模板來實現(xiàn)靜多態(tài)不同,Java基于運行時支持選擇了泛型,兩者的實現(xiàn)原理大相庭徑。C++可以支持基本類型作為模板參數(shù),Java卻只能接受類作為泛
發(fā)表于 09-27 16:50
?0次下載
51單片機C語言的變量和常量如何區(qū)分常量的詳細(xì)資料說明
程序運行過程中不能改變值的量,而變量是可以在程序運行過程中不斷變化的量。變量的定義可以使用所有C51編譯器支持的數(shù)據(jù)類型,而常量的數(shù)據(jù)類型只有整型、浮點型、字符型、字符串型和位標(biāo)量。這
發(fā)表于 07-24 17:37
?0次下載
Java泛型的工作原理和案例
泛型是Java語言一個非常重要的概念,在Java集合類框架中被廣泛應(yīng)用。在介紹泛型之前先看一個例子。
評論