Generic Const Args 与你
摘要
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
深入解释 Go 编译器如何使用 GC shape stenciling 实现泛型,并与 Rust 的 full monomorphization 和 Java 的 type erasure 进行比较。
我们也有命名参数了——家庭版
文章探讨了Rust通过类型系统和结构体模拟命名参数的方法,这是一种无需新增语言特性的便捷替代方案。
命名参数和可选参数非常优秀
本文批判了Rust对命名参数和可选参数的缺失,对比了Dart、C#、TypeScript与Python的实现方式,并展望了Rust未来的潜在解决方案。
参数之争
本文探讨了在编程语言中使用命名参数和灵活参数风格的权衡,比较了 Rust 的简洁性与 Ruby 的简练性,并对在 Rust 中采用类似功能表示怀疑。
写给 Rust 程序员的 C 语言
一篇由 Rust 程序员撰写的博文,记录了其自学 C 语言过程中遇到的各种奇特行为,包括通过 <stdbool.h> 实现布尔类型、以 null 结尾的字符串与 Rust 中长度前缀的 &str 之间的对比,以及相关的内存权衡。对于从 Rust 转向 C 的开发者来说,它是一份实用的配套指南。