【Rust 官方论坛帖子整理】 系列:https://zjp-cn.github.io/rust-note/forum/static-promotion.html
RFC 1414: static promotion
临时引用的静态生命周期提升
在 RFC 1414: rvalue static promotion 中实现了右值的静态生命周期提升功能,也就是将
常量表达式 的右值提升到静态内存 (static memory) 而不是放在栈槽 (stack slot) 中,并通过直接创建
'static
引用来暴露这些值。典型例子是支持类似于下面的代码:
let x: &'static u32 = &42;
fn return_static_ref() -> &'static u32 { &42 }
// 可把 u32 换做任意数据结构
struct Custom(u32);
let x: &'static Custom = &Custom(42);
fn return_static_ref_custom() -> &'static Custom { &Custom(42) }
use std::cell::Cell;
type Enum = Option<Cell<u32>>;
let x: &'static Enum = &None;
fn return_static__ref_none() -> &'static Enum { &None }
换言之,对于 let x: &u32 = &...;
数据 ...
可能被放置在栈上[^&42],而 let x: &'static u32 = &...;
则把数据 ...
放置于静态内存,所以显然后者的生命周期可以比前者长,这对于返回 &'static T
的函数来说尤其有用。
[^&42]: 如果一个 const fn
返回 42,那么该常量函数返回的值不一定在静态区,见最后部分。
此外,RFC 还总结了一条规则:
// 点击右上角按钮,可以正常运行这段代码
use std::mem::size_of;
type T = usize;
const CONST_EXPR: &'static usize = &size_of::<usize>();
// 如果这行编译通过
const X: &'static T = &CONST_EXPR;
// 那么这行也应该编译通过
let x: &'static T = &CONST_EXPR;
不是所有引用都有资格被提升
首先,最重要的前提是,此功能只针对常量表达式的值:常见的常量表达式来源有 literals、constants、statics、const fn
。
其次,在 RFC 中提到了一种特殊情况:
常量表达式中如果含 UnsafeCell { ... }
的构造表达式,那么临时的引用不能被提升为静态生命周期引用。
use std::cell::{Cell, UnsafeCell};
// error[E0515]: cannot return reference to temporary value
fn not_allowed() -> &'static Option<UnsafeCell<u32>> { &Some(UnsafeCell::new(42)) }
// Cell::new(...) 背后使用了 UnsafeCell,所以也无法被提升
fn not_allowed_too() -> &'static Option<Cell<u32>> { &Some(Cell::new(42)) }
这是因为要确保被提升的值在引用后是真正不可变的。
在常量中出现 panic
另一个 RFC 2345: const panic 规定了
panic!
可用于 constants、const fn
等场合(基于它的各种宏 assert*
也可用于同样的场合),并且
其结果为编译时错误。
所以,如果碰到 panic!
的常量表达式,临时的引用无法提升为 'static
:
const fn size_of<T>() -> usize { panic!() }
// RFC 2345 规定的编译失败
// error[E0080]: evaluation of constant value failed
const X: &'static usize = &size_of::<()>();
// 编译失败,无法提升为 'static
// error[E0716]: temporary value dropped while borrowed
let x: &'static usize = &size_of::<()>();
注意,static promotion 所涉及的是引用(准确说是共享引用 &T
)提升生命周期的问题:
const fn size_of<T>() -> usize { panic!() }
// RFC 2345 规定的编译失败
// error[E0080]: evaluation of constant value failed
const X: usize = size_of::<()>();
// 下面的代码与 static promotion 无关
// 编译通过,但运行失败
let x: usize = size_of::<()>();
let x: &usize = &size_of::<()>();
有资格的引用不一定被自动提升
这部分内容似乎没有在 RFC 中被提及。
从一个简单的例子出发。
use std::mem::size_of;
// 下面两行代码符合第一部分提到的规则
const _: &'static usize = &size_of::<usize>();
let _: &'static usize = &size_of::<usize>();
但是,考虑以下内容(下面对 static promotion 简称为“静态提升”):
// 此代码块可直接编辑
fn main() {
// 代码正常
const _: &'static usize = &size_of::<usize>();
// 下面的代码却 **不符合** 第一部分提到的规则
// error[E0716]: temporary value dropped while borrowed
let _: &'static usize = &size_of::<usize>();
}
// 对 std::mem::size_of 进行封装,却无法“静态提升”
const fn size_of<T>() -> usize { std::mem::size_of::<T>() }
// 甚至对于简单的 literal,也无法“静态提升”
// const fn size_of<T>() -> usize { 0 }
回顾第一部分 RFC 提到的规则:
// 如果这行编译通过
const X: &'static T = &CONST_EXPR;
// 那么这行也应该编译通过
let x: &'static T = &CONST_EXPR;
const _: &'static usize = &size_of::<usize>();
这行说明 &size_of::<usize>()
是有资格进行“静态提升”的,而编译器对
let _: &'static usize = &size_of::<usize>();
没能把常量函数 size_of
的返回值视为常量,而是视为一个普通函数的值,所以这个值被放在了栈上,从而是“临时的”,而不是“静态的”。
事实上,对于 const fn
,除非它被赋予 #[rustc_promotable]
属性,否则无法被隐式提升(等会解释这个概念):
std::mem::size_of
具有这个属性,而任何自定义(包括封装)的 size_of
不具有。
有一些方法可以解决,基本逻辑是把常量函数的返回值显式地告知编译器为常量。
注意:
let _: &'static T
与fn f() -> &'static T
在静态提升中差不多,都是把临时引用提升为静态引用。- 链接的例子基于一个关于 vtable 构造技巧的帖子, 本文也是受这则帖子及其讨论梳理出来的。
方法一:const
items
// 此代码块可直接编辑
fn main() {
const X: usize = size_of::<usize>();
// 当然也可以:
// const X: &'static usize = &size_of::<usize>();
let _: &'static usize = &X;
}
const fn size_of<T>() -> usize { std::mem::size_of::<T>() }
// const fn size_of<T>() -> usize { 0 }
这其实是实施静态提升功能前的做法,所以这是最基本的办法。
缺点在于,如果碰到 fn f<T>() -> &'static ...
这类带类型参数的函数,其函数内部的 constants 无法直接使用类型参数(
vtable 例子 )。
// 此代码块可直接编辑
fn f<T>() -> &'static usize {
// error[E0401]: can't use generic parameters from outer function
const X: usize = size_of::<T>();
&X
}
const fn size_of<T>() -> usize { std::mem::size_of::<T>() }
// const fn size_of<T>() -> usize { 0 }
fn main() { }
方法二:associated constants
关联常量有两类定义:impl Type
中、impl Trait for Type
中。
// 此代码块可直接编辑
fn f<T>() -> &'static usize {
struct Type<T>(*mut Self);
impl<T> Type<T> {
const X: usize = size_of::<T>();
}
&Type::<T>::X
}
const fn size_of<T>() -> usize { std::mem::size_of::<T>() }
// const fn size_of<T>() -> usize { 0 }
fn main() { }
// 此代码块可直接编辑
fn f<T>() -> &'static usize {
trait Trait<T> {
const X: usize = size_of::<T>();
}
struct Type;
impl<T> Trait<T> for Type {}
&<Type as Trait<T>>::X
}
const fn size_of<T>() -> usize { std::mem::size_of::<T>() }
// const fn size_of<T>() -> usize { 0 }
fn main() {}
这解决了方法一中的常量无法使用类型参数的问题。(vtable 例子 type-assoc-const 和 trait-assoc-const)
方法三: const
block
// 此代码块可直接编辑,且需要 nightly rustc
#![feature(inline_const)]
fn f<T>() -> &'static usize { const { &size_of::<T>() } }
const fn size_of<T>() -> usize { std::mem::size_of::<T>() }
// const fn size_of<T>() -> usize { 0 }
fn main() {}
这是最简洁的方法。详见 RFC 2920。(vtable 例子)
RFC 3027: infallible promotion
在 RFC 3027 中,规定了以下术语:
- 生命周期延长 (lifetime extension):使引用具有
'static
生命周期 - 常量提升 (promotion):将部分代码提取为常量的底层机制(这个过程是由编译器自动完成的,无需手写)
它们之间的关系是:生命周期延长(即 RFC 1414)是提升的一个主要应用,如果从用户的语法层面看,可以视为:
let _: &'static u32 = &42; // lifetime extension
let _: &'static u32 = {
const X: u32 = 42; // promotion
&X // lifetime extension
};
这是一个相当复杂的 RFC(涉及许多细节问题),并且还在进展中。
与之密切相关的 const-eval 文档进一步阐述了提升的限制,这被称作 promotability,比如
- 必须能把表达式放入所谓的 const:所以不允许比较指针、不允许内部可变性,还要注意
Send
、Drop
、static mut
等问题 - panic、overflow、边界检查不通过等情况下,不能被提升
- 要满足 const safety
- 以及其他一些规则
回到 RFC 3027,其中区分了两种提升:
- 显式提升 (explicit promotion):指必须提升成常量,比如
#[rustc_args_required_const]
和asm!
- 隐式提升 (implicit promotion):指可以提升成常量,比如引用不必是
'static
、数组的 repeat expression 是Copy
或者 repeat count 不超过 1 时
显式提升必须在编译时知道该值,因此如果无法确定该值,停止编译是正确的行为。
而目前唯一稳定的隐式提升形式就是生命周期延长。一个重要事实是 const fn
必须在具有 #[rustc_promotable]
属性的情况下才具备隐式提升的资格,(见 有资格的引用不一定被自动提升 一节)。
此外,隐式提升的一个前提是,表达式必须不能求值失败。这方面有诸多限制,首先需要区分哪些操作一定不会失败,哪些操作可能导致失败(这在 RFC 中具体罗列了,这里不重复列了)。
最终的部分结果是:
- (自定义)
const fn
因不具有#[rustc_promotable]
而不具备隐式提升的资格,如果延长生命周期,必须放入常量中明确要求编译期求值 - 除法[^Division]、取模、索引操作不具备隐式提升的资格,如果延长生命周期,必须放入常量中明确要求编译期求值
解决方案仍然可以参照 有资格的引用不一定被自动提升 一节。
[^Division]: 小细节补充:对于简单的 x/y
,只要 y
不为 0 的整数字面值,其结果依然可以被提升,见 pull/80579:
let _: &'static i32 = &(1 / 2); // ok: 隐式提升
let _: &'static i32 = &((1-1) / 2); // ok: 隐式提升
const _: &'static i32 = &(1 / (1 + 1)); // ok: 明确要求编译期求值
// error[E0716]: temporary value dropped while borrowed
let _: &'static i32 = &(1 / (1 + 1)); // y 需要进一步计算,不支持隐式提升
// error[E0515]: cannot return reference to temporary value
fn f() -> &'static i32 { &(1 / (1 + 1)) } // 理由同上
评论区
写评论感谢科普
Cool!