🦍

Rustのrvalue static promotionについて

2023/01/04に公開

はじめに

去年末に次のツイートをしました。

https://twitter.com/gorilla0513/status/1608622096808308736?s=20&t=LUCHaUIqhHAk9CgpcpV50w

コードは次のようになっています。

#[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.

constexprC++の文脈のようで、こちらによると「コンパイル時に値が決定する定数、コンパイル時に実行される関数、コンパイル時にリテラルとして振る舞うクラスを定義できる」という意味のようです。

つまり「コンパイル時に値を確定できる値式(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のこういった暗黙的な挙動は、背景などを追わないと理解できないこともあり、理解するのが中々大変な言語だなと改めて思いました。
日々精進あるのみ。

参考文献

Discussion