拆分 const 泛型功能

2021 年 9 月 6 日 · lcnr 代表 Const 泛型项目组

在 1.51 版本稳定了 const 泛型的 MVP(最小可行产品)之后,const 泛型项目组持续在 const 泛型上工作。这项工作的很大一部分被 const_genericsconst_evaluatable_checked 特性门所限制。随着时间的推移,const_generics 特性本身变得相当无用,而 const_evaluatable_checked 的名称并不能真正体现该特性旨在做什么。

为了改进这一点,我们最近移除了 const_genericslazy_normalization_constsconst_evaluatable_checked 特性。它们已被 feature(adt_const_params)feature(generic_const_exprs) 所取代。

由于 const 泛型方面有很多进展,这里快速概述一下新的和已有的特性,以及它们需要稳定化还需要做多少工作:

feature(adt_const_params)

在稳定版本中,只允许整数、charbool 作为 const 参数的类型。这个特性允许使用额外的类型,例如 &'static str 和用户自定义类型。

#![feature(adt_const_params)]

#[derive(PartialEq, Eq)]
enum ImageFormat {
    Rgb8,
    Rgba8,
    // ...c
}

struct Image<const FORMAT: ImageFormat> {
    // ...
}

impl Image<{ ImageFormat::Rgba }> {
    fn alpha(&self, pixel: PixelLocation) -> u8 {
        // ...
    }
}

注意,即使有了这个特性,泛型 const 参数类型,例如 struct Foo<T, const N: T> { ... },仍然是被禁止的。虽然允许这类特性是期望的,但这增加了超出我们当前能力的额外复杂性。

稳定化仍有两个主要障碍:

第一个障碍是 向 valtrees 的过渡。Valtrees 是一种将值表示为带有整数节点的树的结构,简化了我们与更复杂类型交互的方式。

此外,我们必须确定我们究竟想允许哪些类型作为 const 参数类型。这与关于 “结构化匹配” 的讨论相关,该讨论仍在进行中。

虽然上述问题肯定不是微不足道的,但这个特性完全有可能在几个月内准备好进行稳定化。

feature(generic_const_exprs)

在没有任何不稳定特性时,const 参数必须是完全具体的表达式或其本身是一个泛型参数,因此像 N + 1 这样的常量是被禁止的。有了这个特性,使用泛型参数的表达式成为可能。

#![feature(generic_const_exprs)]

fn split_first<T, const N: usize>(arr: [T; N]) -> (T, [T; N - 1]) {
    // ...
}

struct BitSet<const SIZE: usize>
where
    [u8; (SIZE + 7) / 8]: Sized,
{
    storage: [u8; (SIZE + 7) / 8],
}

我们目前要求用户添加界限(bounds),断言泛型常量能够成功求值。对于项的 API 中可见的所有常量,这些界限会被隐式添加。

如果类型为 Foo 的常量表达式 expr 在其他情况下不会在 where 子句或函数签名中使用,我们会在项的 where 子句中添加一个提及 expr 的,否则无关紧要的界限。为此,可以定义一个 struct Evaluatable<const N: Foo>; 并使用 Evaluatable<{ expr }>: 作为界限。如果 expr 的类型是 usize,我们倾向于使用 [u8; expr]:[u8; expr]: Sized。虽然我们未来极有可能为这些界限添加专门的语法,但我们正在等待,直到这个特性的其余部分更加成熟。

这个特性距离稳定还很远,并且有一些 重大的未解决问题。特别是对于 where 界限内的常量,有很多细微的 bug 和向后不兼容问题需要解决,然后我们才能考虑如何稳定化它。

feature(const_generics_defaults)

类似于类型参数的默认值,这个特性增加了为 const 参数声明默认值的能力。

#![feature(const_generics_defaults)]

struct ArrayStorage<T, const N: usize = 2> {
    arr: [T; N],
}

impl<T> ArrayStorage<T> {
    fn new(a: T, b: T) -> ArrayStorage<T> {
        ArrayStorage {
            arr: [a, b],
        }
    }
}

为了允许类型参数默认值与 const 参数在同一个列表中,我们还打算移除对类型参数和 const 参数的顺序限制,允许 struct Foo<const N: usize, T = [u32; N]> { ... }

这个特性基本上已经准备好进行稳定化,目前正被阻碍在找出稳定化报告中任何潜在的边缘情况。

feature(generic_arg_infer)

虽然在函数体内部已经可以使用通配符 _ 来表示类型参数,但 const 参数却不能。这个特性为常量增加了这个能力。

#![feature(generic_arg_infer)]
fn array_from<T, U, const N: usize>(arr: [T; N]) -> [U; N]
where
    U: From<T>,
{
    arr.map(From::from)
}

fn main() {
    let x = ["this", "is", "a", "six", "element", "array"];
    // using `_` for the parameter `N` lets
    // the compiler infer the correct value
    let _y = array_from::<_, String, _>(x);
}

这个特性尚未准备好进行稳定化,尽管目前没有已知的重大阻碍。然而,为了有信心地稳定化它,我们可能需要进行一些大规模的重构,因为目前的设置在某些方面感觉相当脆弱。