Temporary Object の寿命

  • 11
    Like
  • 0
    Comment

RefCell を使っていて Temporary Object の寿命を意識する瞬間があった.

use std::cell::RefCell;
use std::ops::DerefMut;

struct S { i: i32 }

fn main() {
    let cell = RefCell::new(S {i: 777});

    // a. これはコンパイルエラーになる.
    let borrowed = &mut cell.borrow().deref().i;

    // b. これはコンパイルエラーにならない.
    let borrowed = &mut cell.borrow().i;
}

a と b でやっている事はほとんど同じだ.

  1. RefCell から Ref<S> を取得
  2. Ref<S> から生の参照 &S を取得
  3. &S からメンバへの参照 &i を取得
  4. それを borrowed へ代入

違うのは a が deref() を よんで Ref<S> から生の参照を得ているのに対し, b が deref coercions を使って Ref<S> から直接メンバ i への参照を得ている点だ.b で省略された deref() 呼び出しはコンパイラによって挿入されているので,実行時に deref() が呼び出されていることに変わりはない.
字面上の違いしかないはずなのに,a だけがコンパイルエラーになるのはなぜだろう?

公式ドキュメントを読んでみると,Ref<S> の寿命が a と b で違うことが原因のようだ.
Ref<S> は変数に代入されていない一時オブジェクトで,通常その寿命は一時オブジェクトが生成されたステートメント内に限定される.

例で言えば let borrow = ... 文が終わった時点で Ref<S> の寿命はつきる.一方 &i は borrowed に代入しているので,このブロックが終わるまで生存している.
つまり Ref<S>&i の寿命を比較すると Ref<S> < &i だ.deref() の定義をみると返した参照の寿命は Ref<S> の寿命よりも短くなければいけないと書いてあり,Ref<S> < &i はこれに違反している.これが a がコンパイルエラーとなる理由だ.

では b はなぜコンパイルエラーにならないのか.実は一時オブジェクトの寿命には別のルールがある.

When a temporary rvalue is being created that is assigned into a let declaration, however, the temporary is created with the lifetime of the enclosing block instead, as using the enclosing statement (the let declaration) would be a guaranteed error (since a pointer to the temporary would be stored into a variable, but the temporary would be freed before the variable could be used). The compiler uses simple syntactic rules to decide which values are being assigned into a let binding, and therefore deserve a longer temporary lifetime.
https://doc.rust-lang.org/reference.html#temporary-lifetimes

もし作られた一時オブジェクトが let で代入されるなら,一時オブジェクトは囲んでいるステートメントではなく囲んでいるブロックと同じ寿命を持つ.そしてそれを判断するのにコンパイラは単純な文法的ルールを使う.と書いてある.これだけではピンとこないが,リンク先の公式ドキュメントにはいくつかの例とともにテンポラリオブジェクトの寿命がステートメントになるかブロックになるかが説明されている.

a では,コンパイラは borrow() で作られた Ref<S> 一時オブジェクトが deref() 関数に引数として渡されているとみなす.コンパイラ的に let に代入されているのはあくまで deref() の戻り値なので,Ref<S> の寿命はステートメント内となる.
一方 b ではコンパイラは Ref<S> 一時オブジェクトのメンバへの参照が let で代入されているとみなす.そのため Ref<S> の寿命はブロック内となっている.

一時オブジェクトの寿命を判断する基準はランタイム時の挙動(deref coercions されるとか)ではなく,たんに字面を見て関数呼び出しの形(let v = &temp.func();)ならステートメント内の寿命,メンバ参照の形 (let v = &temp.field;) ならブロック内の寿命となるようだ.