常量求值
常量求值是在编译过程中计算 表达式 结果的过程。所有表达式中只有一部分可以在编译时求值。
常量表达式
某些形式的表达式,称为常量表达式,可以在编译时求值。
在 常量上下文 中的表达式必须是常量表达式。
常量上下文中的表达式总是在编译时求值。
在常量上下文之外,常量表达式 可能 会被求值,但不保证一定在编译时求值。
如果一个值必须在编译时(即在常量上下文中)求值,那么诸如越界 数组索引 或 溢出 之类的行为将是编译器错误。否则,这些行为只是警告,但在运行时很可能会产生 恐慌。
只要所有操作数也都是常量表达式,且不会导致任何 Drop::drop 调用被运行,以下表达式就是常量表达式。
- 字面量。
- 常量参数。
-
指向 静态项 的路径,具有以下限制:
- 在任何常量求值上下文中都不允许对
static项进行写入。 - 在任何常量求值上下文中都不允许从
extern静态项中读取。 - 如果求值 不是 在
static项的初始化器中进行的,那么就不允许从任何可变的static中读取。可变的static是指static mut项,或者具有内部可变类型的static项。
这些要求仅在常量求值时检查。换句话说,只要这些访问从未被执行,它们在语法上出现在常量上下文中是允许的。
- 在任何常量求值上下文中都不允许对
- 字段 表达式。
- 不从环境中捕获变量的 闭包表达式。
-
所有形式的 借用,包括原始借用,但以下表达式的借用除外(这些表达式的临时作用域会被延长(见 临时变量生命周期延长)至程序结束):
- 可变借用。
- 对产生具有 内部可变性 的值的表达式的共享借用。
#![allow(unused)] fn main() { // 由于处于尾部位置,此借用将临时变量的作用域延长至程序结束。 // 由于借用是可变的,这在常量表达式中是不允许的。 const C: &u8 = &mut 0; // ERROR not allowed }#![allow(unused)] fn main() { // 常量块类似于常量项的初始化器。 let _: &u8 = const { &mut 0 }; // ERROR not allowed }#![allow(unused)] fn main() { use core::sync::atomic::AtomicU8; // 这是不允许的,因为 1) 临时作用域延长到了程序结束,且 2) 临时变量具有内部可变性。 const C: &AtomicU8 = &AtomicU8::new(0); // ERROR not allowed }#![allow(unused)] fn main() { use core::sync::atomic::AtomicU8; // 同上。 let _: &_ = const { &AtomicU8::new(0) }; // ERROR not allowed }#![allow(unused)] fn main() { #![allow(static_mut_refs)] // 尽管此借用是可变的,但它不是对临时变量的借用,因此这是允许的。 const C: &u8 = unsafe { static mut S: u8 = 0; &mut S }; // OK }#![allow(unused)] fn main() { use core::sync::atomic::AtomicU8; // 尽管此借用是对具有内部可变性的值的借用,但它不是对临时变量的借用,因此这是允许的。 const C: &AtomicU8 = { static S: AtomicU8 = AtomicU8::new(0); &S // OK }; }#![allow(unused)] fn main() { use core::sync::atomic::AtomicU8; // 这种对内部可变临时变量的共享借用是允许的,因为其作用域没有被延长。 const C: () = { _ = &AtomicU8::new(0); }; // OK }#![allow(unused)] fn main() { // 尽管借用是可变的,且临时变量因提升而存续到程序结束,但这是允许的,因为 // 借用不在尾部位置,因此临时变量的作用域不会通过临时变量生命周期延长来延长。 const C: () = { let _: &'static mut [u8] = &mut []; }; // OK // ~~ // 提升的临时变量。 }注意
换句话说 —— 为了关注什么是允许的而不是什么是不允许的 —— 只有当被借用的 位置表达式 是 瞬态的 、 间接的 或 静态的 时,才允许在 常量上下文 中对内部可变数据进行共享借用和可变借用。
如果位置表达式是当前常量上下文的局部变量,或者是临时作用域包含在当前常量上下文中的表达式,则该位置表达式是 瞬态的 。
#![allow(unused)] fn main() { // 借用是对初始化器局部变量的借用,因此此位置表达式是瞬态的。 const C: () = { let mut x = 0; _ = &mut x; }; }#![allow(unused)] fn main() { // 借用是对作用域未延长的临时变量的借用,因此此位置表达式是瞬态的。 const C: () = { _ = &mut 0u8; }; }#![allow(unused)] fn main() { // 当临时变量被提升但没有延长生命周期时,其位置表达式仍被视为瞬态的。 const C: () = { let _: &'static mut [u8] = &mut []; }; }如果位置表达式是 解引用表达式,则该位置表达式是 间接的 。
#![allow(unused)] fn main() { const C: () = { _ = &mut *(&mut 0); }; }如果位置表达式是一个
static项,则该位置表达式是 静态的 。#![allow(unused)] fn main() { #![allow(static_mut_refs)] const C: &u8 = unsafe { static mut S: u8 = 0; &mut S }; }注意
这些规则的一个令人惊讶的后果是我们允许这样做:
#![allow(unused)] fn main() { const C: &[u8] = { let x: &mut [u8] = &mut []; x }; // OK // ~~~~~~~ // 即使在可变借用之后,空数组也会被提升。 }但我们不允许类似的这段代码:
#![allow(unused)] fn main() { const C: &[u8] = &mut []; // ERROR // ~~~~~~~ // 尾部表达式。 }它们之间的区别在于,在第一种情况中,空数组被 提升 了,但它的作用域没有经历 临时变量生命周期延长,所以我们认为 位置表达式 是瞬态的(即使在提升之后该位置确实存续到程序结束)。在第二种情况中,空数组临时变量的作用域确实经历了生命周期延长,因此它因为是对生命周期延长的临时变量的可变借用(从而借用了非瞬态的位置表达式)而被拒绝。
这种效果令人惊讶,因为在这种情况下,临时变量生命周期延长导致可编译的代码比没有它时更少。
见 issue #143129 了解更多细节。
-
#![allow(unused)] fn main() { use core::cell::UnsafeCell; const _: u8 = unsafe { let x: *mut u8 = &raw mut *&mut 0; // ^^^^^^^ // 对可变引用的解引用。 *x = 1; // 对可变指针的解引用。 *(x as *const u8) // 对常量指针的解引用。 }; const _: u8 = unsafe { let x = &UnsafeCell::new(0); *x.get() = 1; // 对内部可变值的修改。 *x.get() }; }
- 分组 表达式。
- 转换 表达式,除了
- 指针到地址转换以及
- 函数指针到地址转换。
- 调用 常量函数 和常量方法。
常量上下文
常量上下文 是以下之一:
作为类型一部分使用的常量上下文(数组类型和重复长度表达式以及常量泛型参数)只能限制性地使用周围的泛型参数:此类表达式必须要么是单个裸常量泛型参数,要么是不使用任何泛型的任意表达式。
常量函数
常量函数 是可以从常量上下文中调用的函数。它使用 const 限定符定义,并且还包括 元组结构体 和 元组枚举变体 构造函数。
例子
#![allow(unused)] fn main() { const fn square(x: i32) -> i32 { x * x } const VALUE: i32 = square(12); square(12); }
当从常量上下文中调用时,常量函数由编译器在编译时解释。这种解释发生在编译 target 的环境中,而不是宿主环境。因此,如果你针对 32 位系统进行编译,usize 就是 32 位,而不管你是在 64 位还是 32 位系统上进行构建。
当在常量上下文之外调用常量函数时,它的行为与没有 const 限定符时相同。
常量函数的主体只能使用 常量表达式。
常量函数不允许是 异步 的。
常量函数的参数类型和返回类型仅限于那些与常量上下文兼容的类型。