Rustのrvalue static promotionについて
はじめに
去年末に次のツイートをしました。
コードは次のようになっています。
#[derive(Debug)]
struct Foo {}
fn foo<'a>() -> &'a Foo {
let foo = &Foo {};
foo
}
fn main() {
dbg!(foo());
}
この例ではコンパイルはできる、というのが答えです。
この記事では「なぜコンパイルできるのか」について解説していきます。
コンパイルできる理由
Rvalue static promotion
により、Foo
は静的領域に保持され、どこからでも参照できるためです。
Rvalue static promotionとは
- 簡潔に説明すると「コンパイル時に確定できる値かつその値は不変」の場合は静的領域に値を確保し、参照できるようにする
-
rvalue
というのはplace expression
以外の式のことを指す-
place expression
というのはメモリ位置を表現した式のこと - たとえば
let a: &static u32 = &32
の左辺はplace expression
、右辺がvalue expression
- ただし、左辺が
place expression
になるとは限らず、1 + a
というように左辺がvalue expression
という場合もある - 具体的な定義はReferenceに次のように定義されている
A place expression is an expression that represents a memory location. These expressions are paths which refer to local variables, static variables, dereferences (*expr), array indexing expressions (expr[expr]), field references (expr.f) and parenthesized place expressions. All other expressions are value expressions.
- 次のコードが
'static
にできる・できない例let a: &'static u32 = &32; let b: &'static Option<UnsafeCell<u32>> = &None; let c: &'static Fn() -> u32 = &|| 42; let h: &'static u32 = &(32 + 64); fn generic<T>() -> &'static Option<T> { &None::<T> } // BAD: let f: &'static Option<UnsafeCell<u32>> = &Some(UnsafeCell { data: 32 }); let g: &'static Cell<u32> = &Cell::new(); // assuming conf fn new()
-
Rvalue static promotionの詳細
RFCにはpromoteできる条件として、次の記述があります。
If a shared reference to a constexpr rvalue is taken. (&<constexpr>)
And the constexpr does not contain a UnsafeCell { ... } constructor.
And the constexpr does not contain a const fn call returning a type containing a UnsafeCell.
Then instead of translating the value into a stack slot, translate it into a static memory location and give the resulting reference a 'static lifetime.
constexpr
はC++
の文脈のようで、こちらによると「コンパイル時に値が決定する定数、コンパイル時に実行される関数、コンパイル時にリテラルとして振る舞うクラスを定義できる」という意味のようです。
つまり「コンパイル時に値を確定できる値式(value expression)」と解釈すればよいかなと思います。
ちなみに、UnsafeCell
は内部可変性を可能性にするので、それを使っていると値は不変ではなくなってしまうため、UnsafeCell
を含まないことを条件としています。
改て冒頭のコードを見ると、foo()
はコンパイル時に「値が確定できる」ため、コンパイルが通るということになります。
fn foo<'a>() -> &'a Foo {
let foo = &Foo {};
foo
}
fn main() {
dbg!(foo());
}
では、渡された引数を含むFoo
を返すとどうなるでしょうか?
答えは次のように、コンパイルエラーになります。
$ cat src/main.rs
#[derive(Debug)]
struct Foo {
value: isize,
}
fn new_foo<'a>(value: isize) -> &'a Foo {
let foo = &Foo { value };
foo
}
fn main() {
dbg!(new_foo(10).value);
}
$ cargo run
Compiling foo v0.1.0 (/Users/skanehira/dev/github.com/skanehira/sandbox/rust/foo)
error[E0515]: cannot return value referencing temporary value
--> src/main.rs:8:5
|
7 | let foo = &Foo { value };
| ------------- temporary value created here
8 | foo
| ^^^ returns a value referencing data owned by the current function
For more information about this error, try `rustc --explain E0515`.
error: could not compile `foo` due to previous error
これは引数を含むことによって「コンパイル時に値を確定できない」ため、スタックにFoo
を置くようになり、スタックにあるFoo
への参照を返せないためと解釈すればわかりやすいかと思います。
最後に
Rustのこういった暗黙的な挙動は、背景などを追わないと理解できないこともあり、理解するのが中々大変な言語だなと改めて思いました。
日々精進あるのみ。
参考文献
- https://rust-lang.github.io/rfcs/1414-rvalue_static_promotion.html
- https://speakerdeck.com/qnighy/sofalseshi-zhi-desuka-chang-suo-desuka
- https://cpprefjp.github.io/lang/cpp11/constexpr.html
- https://github.com/rust-lang/rust/issues/38865
- https://keens.github.io/blog/2020/12/15/rustnoconst_fnttenani_/
Discussion