Generic Const Args 与你

Lobsters Hottest 工具

摘要

Rust 项目宣布推出 Generic Const Arguments(GCA),这是一组新的语言特性,允许在 Const Generics 中使用包含泛型参数的表达式,取代长期处于实验状态的 `generic_const_exprs` 特性。文章介绍了 `gca!` 宏、`gca_adts` 特性、设计权衡,以及 Const Generics 项目组成员的贡献。

<p><a href="https://lobste.rs/s/6jcvdn/generic_const_args_you">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/10/02 10:35

# Generic Const Args 与你 | Inside Rust Blog Source: https://blog.rust-lang.org/inside-rust/2026/10/02/generic-const-args-and-you/ ## Generic Const Args 与你 2024 年 6 月在 RustFest Zürich 上,Const Generics 项目组首次讨论了一个新的设计,用于在 Const Generics 中支持更复杂的泛型参数用法。从那时起,我们不断完善最初的初始设计,并将这个新设计实现为一组被称为 "Generic Const Arguments"(简称 GCA)的特性。这些特性旨在取代现有的 `generic_const_exprs` 特性——该特性自 2021 年 `min_const_generics` 稳定以来,一直以某种形式存在。 尽管 GCA 使 `generic_const_exprs` 变得不再必要,但我们在这上面投入的时间依然极具价值,因为 GCA 的设计和实现在很大程度上受到了 `generic_const_exprs` 的启发。GCA 家族中的每一个特性,都引入了对某类特定表达式的支持,使其能在 Const Generics 中与泛型参数一起使用。本文将介绍 GCA 家族的全部特性,并解释它们的设计思路以及使用方式。 --- 特别感谢 @camelid (https://www.github.com/camelid) 完成了 GCA 原型几乎全部的初始实现工作。没有他,GCA 大概率至今仍只是一个模糊的概念,而不是任何具体可感的东西。 另外也要特别感谢 @khyperia (https://github.com/khyperia),他为从最初的 GCA 原型走向如今这个功能齐全的特性家族,投入了大量的实现与设计工作。 最后还有许多其他人也为此做出了贡献。如需查看所有人及其贡献的完整清单,请参阅 Full Const Generics (https://github.com/rust-lang/goals/issues/100) 项目目标。 ## 什么是 GCA 在稳定版上,Const Generics 仅支持泛型参数本身(例如 `{ N }`)或完全具体的常量(例如 `{ 1 + 1 })。我们无法写出像 `[u8; T::NUM_BYTES]` 或 `[u8; N + 1]` 这样的数组。这一限制阻碍了许多有用的抽象,也迫使用户转而依赖 typenum crate。 Generic Const Arguments 是一组特性,它们为 Const Generics 引入了对涉及泛型参数的更多种表达式的支持。例如,`gca_adts` 特性为 Const Generics 添加了对包含泛型参数的结构体表达式(Struct Expressions)的支持: ```rust #![feature(adt_const_params, gca_adts)] use core::gca; #[derive(ConstParamTy, Eq, PartialEq)] struct Bar(usize); struct Foo { field: Bar, } fn accepts<const N: Foo>() {} fn example() { accepts::<gca!(Foo { field: N })>(); } ``` 在这个例子中,`gca!(Foo { field: N })` 就是 `gca_adts` 所引入的新功能。`adt_const_params` 特性是独立的,它允许我们定义 `const N: Foo` 这样的泛型参数。 所有 GCA 特性都要求把新支持的表达式写在 `gca!` 宏调用之内。我们已经意识到这一要求会带来显著的易用性问题,目前也在探索如何避免它,不过还没有找到一个满意的方案(稍后详谈)。 `gca!(..)` 常量实参的语义与其他常量实参略有不同。首先,`gca!(..)` 常量实参允许在其中使用泛型参数,而普通的常量实参通常是不允许这样做的。在上面的例子中,在 `gca!(Foo { field: N })` 中使用常量参数 `N` 是合法的,因为它位于 `gca!(..)` 表达式之内;如果我们写成 `Bar<{ Foo { field: N } }>`,编译器就会报错。 其次,`gca!(..)` 常量实参所支持的表达式种类,比普通常量实参要少得多。例如在撰写本文时,`gca!` 实参尚不支持算术运算或函数调用。写 `gca!(1 + 1)` 或 `gca!(foo())` 会导致错误,而直接写 `{ 1 + 1 }` 或 `{ foo() }` 则不会。要在使用泛型参数的同时使用这类不被支持的表达式,可以通过 `gca_const_items` 特性来实现,我们稍后会详细讨论。 ### ADT 泛型常量实参 对数组、元组以及 ADT 的构造支持都归入了 `gca_adts` 特性。我们将来可能会把它们拆分为多个特性,但目前就只有这一个。 无论 enum 和 struct 使用哪种语法定义(即 unit、tuple 还是 struct 语法),对它们的构造都受到支持: ```rust #![feature(gca_adts, adt_const_params)] use core::gca; #[derive(ConstParamTy, Eq, PartialEq)] struct Foo(usize); const N: usize = 1; #[derive(ConstParamTy, Eq, PartialEq)] struct TupleStruct(usize); fn accepts(x: TupleStruct) {} fn example() { accepts(TupleStruct(gca!(N))); } ``` ```rust #![feature(gca_adts, adt_const_params)] use core::gca; #[derive(ConstParamTy, Eq, PartialEq)] struct Foo(usize); const N: usize = 1; #[derive(ConstParamTy, Eq, PartialEq)] enum MyEnum { Record { x: usize }, } fn accepts(x: MyEnum) {} fn example() { accepts(gca!(MyEnum::Record { x: N })); } ``` 最后是数组和元组的构造支持: ```rust #![feature(gca_adts, adt_const_params)] use core::gca; const N: usize = 1; fn accepts_arrays(_x: [u8; 2]) {} fn example() { accepts_arrays(gca!([0x1u8; N])); } ``` ```rust #![feature(gca_adts, adt_const_params)] use core::gca; const N: usize = 1; fn accepts_tuple(_x: (usize, usize)) {} fn example() { accepts_tuple(gca!((0x1usize, N))); } ``` 目前我们尚不支持数组重复表达式(array repeat expressions),但计划将来会支持: ```rust #![feature(gca_adts, adt_const_params)] use core::gca; const N: usize = 1; fn accepts_arrays(_x: [usize; N]) {} fn example() { // 目前不被允许 :( accepts_arrays(gca!([N; N])); } ``` ### Const Item 泛型常量实参 支持在类型系统中使用 const item,是 `gca_const_items` 特性的一部分。该特性还要求启用 `-Znext-solver` 不稳定编译器标志。 通过允许任意 const item 在类型系统中使用,我们实际上允许了任意表达式*间接地*在类型系统中被使用。例如 `gca!(N + 1)` 无法在类型系统中使用,但一个定义为 `const FOO: usize = N + 1;` 的 const item 却可以。将关联常量用作常量实参的形式如下: ```rust #![feature(gca_const_items)] use core::gca; trait Trait { const ASSOC: usize; } fn example<T: Trait>() -> [u8; gca!(T::ASSOC)] { [0x1; gca!(T::ASSOC)] } ``` 使用自由 const item 或固有(inherent)关联 const item 的写法与之类似: ```rust #![feature(gca_const_items, generic_const_items, inherent_associated_types)] use core::gca; const FREE: usize = size_of::<u8>(); fn example_free() -> [u8; gca!(FREE)] { [0x6; gca!(FREE)] } struct Foo<T>(T); impl<T> Foo<T> { const INHERENT: usize = size_of::<T>(); } fn example_inherent<T>(foo: Foo<T>) -> [u8; gca!(Foo::<T>::INHERENT)] { [0x7; gca!(Foo::<T>::INHERENT)] } ``` 注意,上面的例子中还启用了 `inherent_associated_types` 特性门控。在 `gca!(..)` 宏中使用固有关联常量需要额外的特性门控,因为支持它所面临的挑战与支持固有关联类型时是类似的。 在定义 const item 时,右侧可以是一个 `gca!(..)` 表达式,表示类型系统可以对该表达式的确切形式进行推理。若不加这层,该常量就会被"不透明地"对待,类型系统对它的泛型用途所能做的推理也就非常有限: ```rust #![feature(gca_const_items, generic_const_items)] use core::gca; const N: usize = 1; const FREE_OPAQUE: usize = N; fn make_array1() -> [u8; FREE_OPAQUE] { // ERROR: `FREE_OPAQUE` 和 `N` 不相等 [0; N] } const FREE_GCA: usize = gca!(N); fn make_array2() -> [u8; FREE_GCA] { // OK :3 [0; N] } ``` 在上面的例子中,`FREE_GCA` 常量的右侧被定义为 `gca!(..)` 表达式,这使得类型系统能够透明地推理这个 const item。这让编译器得以判断 `FREE_GCA` 与 `N` 是等价的,因此类型 `[u8; N]` 与 `[u8; FREE_GCA]` 相等。 另一方面,`FREE_OPAQUE` 常量并未被定义为拥有 `gca!(..)` 右侧,所以类型系统会"不透明地"对待它,无法推理出它等价于 `N`。最终,这会导致类型 `[u8; N]` 与 `[u8; FREE_OPAQUE]` 被判定为不相等。 有了类型系统中 const item 的支持,`gca_const_items` 还支持关联 const 绑定,并允许带有关联常量的 trait 实现 dyn 兼容: ```rust #![feature(gca_const_items)] use core::gca; trait Trait { const ASSOC: usize; } fn make_dyn<T: Trait>(val: T) -> Box<dyn Trait> { Box::new(val) } ``` 在稳定版上,上面的例子会因两个原因而编译失败。首先,与关联类型不同,我们不支持对关联常量做限定(例如 `T: Trait`);其次,与关联类型不同,我们不允许为带有关联常量的 trait 创建 `dyn` 类型。启用 `gca_const_items` 后,这两点都得到了支持。 ### Minimal Const Item 泛型常量实参 我们还有一个 `gca_const_items` 特性的精简版本:`gca_min_const_items`。它支持的内容与完整特性大体相同,只是它不支持*所有* const item,而只支持那些定义为 `gca!(..)` 表达式的 const item。使用该特性*不需要*启用不稳定的 `-Znext-solver` 标志。 ```rust #![feature(gca_min_const_items, generic_const_args)] use core::gca; const N: usize = 1; const BAD: usize = N; const GOOD: usize = gca!(N); fn foo() { // 不 OK!`BAD` 与 `gca!(..)` 表达式不相等 let _ = [0u8; BAD]; // OK :3 let _ = [0u8; GOOD]; } ``` 与完整的 `gca_const_items` 特性不同——后者会接受上面的例子——`gca_min_const_items` 会拒绝它,因为 `BAD` 并没有使用 `gca!(..)` 右侧。 为了支持 trait 关联常量,存在一个 `rustc_always_gca` 属性[^1],它强制要求某个关联常量必须始终以 `gca!(..)` 表达式的形式实现。 ```rust #![feature(gca_min_const_items)] use core::gca; const N: usize = 1; trait Trait { #[rustc_always_gca] const ASSOC: usize; } impl Trait for () { // 不 OK!不是 `gca!(..)` 表达式 const ASSOC: usize = N; // OK! const ASSOC: usize = gca!(N); } ``` 要求类型系统中的所有 const item 都必须以 `gca!(..)` 表达式的形式定义,让我们能够以一些理想的方式限定 Const Generics 的表达能力。例如,`gca_const_items` 会引入一种可能性:来自 Const Generics 的后单态化(post-mono)错误。就撰写本文时而言,在使用 Const Generics 时,其他情况下并不会出现这类错误。而 `gca_min_const_items` 以牺牲部分表达能力为代价,排除了 `gca_const_items` 中可能导致这些错误的部分。 再举一个例子:完整的 `gca_const_items` 特性有时会产生相当令人困惑的错误——两个常量被判定为不相等,而作为人类的我们显然能看出它们是相等的。相比之下,`gca_min_const_items` 在判断两个常量是否相等时有着相当直接、没有任何意外的规则。 最后,`gca_min_const_items` 的实现难度也远低于 `gca_const_items`,这意味着它应当能在远早于 `gca_const_items` 的时间点达到稳定所需的成熟度。 ## 什么是 Macroless 虽然 GCA 特性家族目前引入了新的显式 `gca!(..)` 常量实参,但从长远来看,这种*显式*写法是不可取的,因为它会在大量使用 Const Generics 的代码中造成严重的易用性问题。然而,在隐式插入 `gca!(..)` 宏这件事上,无论在设计还是实现层面都有不少复杂性。 首先从实现层面看,编译器很难准确判断某个实参何时需要是一个 `gca!(..)` 常量实参。采用天真的语法判断方法会产生假阳性:某些实参被错误地认定为 `gca!(..)` 实参,进而引发连锁错误。 ```rust #![feature(gca_adts, gca_macroless_args)] use core::gca; struct Foo(usize); fn Bar(_: usize) -> usize { 1 } fn accepts_usize(x: usize) {} fn accepts_foo(x: Foo) {} fn example() { const ONE: usize = 1; accepts_foo(Foo(N)); accepts_usize(Bar(ONE)); } ``` 在这个例子中,`Foo(N)` 与 `Bar(ONE)` 在语法上完全等价,但 `Foo(N)` *必须*是一个 `gca!(..)` 常量实参,因为它包含指向泛型参数(`N`)的路径;而 `Bar(ONE)` *不能*是 `gca!(..)` 常量实参,因为它是函数调用,而函数调用在 `gca!(..)` 常量实参中并不受支持。 其次,从设计角度看,让 `gca!(..)` 常量实参变成隐式的,会模糊"哪些常量实参是被接受的"这一界限。如果没有 `gca!(..)`,我们就无法清晰区分哪些常量实参可以使用泛型参数、哪些不可以。这也意味着,`gca!(..)` 常量实参在公共 API 中"不被更改"的 SemVer 承诺会变成隐式的,可能导致意外的破坏性变更。 为了让我们能够独立于 GCA 的核心语义难题来处理这些复杂性,隐式添加 `gca!(..)` 宏的功能被放在了单独的特性门控中,而不是作为主 GCA 特性的一部分。目前与此相关的特性有两个:`gca_macroless_args` 和 `gca_macroless_items`。这两个特性会在当前需要显式写 `gca!(..)` 的不同位置上,分别隐式添加该宏。 这些特性非常实验性,效果也并不好,坦白讲,我们目前大概会建议*不要*使用它们,而*建议*暂且继续使用显式的 `gca!(..)` 常量实参。 同样值得注意的是,尽管在这些特性下 `gca!(..)` 是隐式的,常量实参仍然受到与手写 `gca!(..)` 相同的限制。这意味着,例如 macroless 并不会引入将 `N + 1` 写成常量实参的支持,因为 `gca!(N + 1)` 同样

相似文章

Go 泛型中的 GC shape stenciling

Hacker News Top

深入解释 Go 编译器如何使用 GC shape stenciling 实现泛型,并与 Rust 的 full monomorphization 和 Java 的 type erasure 进行比较。

命名参数和可选参数非常优秀

Lobsters Hottest

本文批判了Rust对命名参数和可选参数的缺失,对比了Dart、C#、TypeScript与Python的实现方式,并展望了Rust未来的潜在解决方案。

参数之争

Lobsters Hottest

本文探讨了在编程语言中使用命名参数和灵活参数风格的权衡,比较了 Rust 的简洁性与 Ruby 的简练性,并对在 Rust 中采用类似功能表示怀疑。

写给 Rust 程序员的 C 语言

Lobsters Hottest

一篇由 Rust 程序员撰写的博文,记录了其自学 C 语言过程中遇到的各种奇特行为,包括通过 <stdbool.h> 实现布尔类型、以 null 结尾的字符串与 Rust 中长度前缀的 &str 之间的对比,以及相关的内存权衡。对于从 Rust 转向 C 的开发者来说,它是一份实用的配套指南。