[Rust] 一時変数に関する非直感的な挙動
作成日:2022.11.27
最終更新日:2023.01.27
タグ: Rust
はじめに
本記事はバージョン1.65.0のrustcのもとで書かれている。
Rustはコンパイラがとても親切なので甘えがちだが、実行時エラーが起きたら自ら原因を究明しなければならない。
今回、実行時エラーによって一時的な構造体がdropされるタイミングが(筆者にとって)直感に反する場合があることに気付かされたので、そのことについて共有する。
なお例示にはif-let式とRefCellを用いているが、if-let式がwhile-let式やmatch式、RefCellがMutexやRwLockでも同様であることを明記しておく。
目次
遭遇した問題
例えば以下のコードは動く。
fn main() { let mut vec = vec![(Some(42), 0), (None, 1)]; if let Some(x) = vec[0].0 { vec.remove(0); vec.push((Some(x), x)); } }
しかし同じ気持ちで以下のコードを書くと動かない。
fn main() { let cell = core::cell::RefCell::new((Some(42), 0)); if let Some(x) = cell.borrow().0 { cell.borrow_mut().1 = x; // thread 'main' panicked at 'already borrowed: BorrowMutError', src\main.rs:4:14 }; }
解決方法
Internals Forum や Users Forum で述べられているように、if-letの右辺(scrutinee)を一度変数に束縛すれば動く。
fn main() { let cell = core::cell::RefCell::new((Some(42), 0)); if let Some(x) = { let tmp = cell.borrow().0; tmp } { cell.borrow_mut().1 = x; } }
fn main() { let cell = core::cell::RefCell::new((Some(42), 0)); let tmp = cell.borrow().0; if let Some(x) = tmp { cell.borrow_mut().1 = x; } }
原因
パニックを起こしたコードの5行目のセミコロンを取ると次のようなエラーとなる。
3 | if let Some(x) = cell.borrow().0 { | ^^^^^^^^^^^^^ | | | borrowed value does not live long enough | a temporary with access to the borrow is created here ... ... 6 | } | - | | | `cell` dropped here while still borrowed | ... and the borrow might be used here, when that temporary is dropped and runs the destructor for type `std::cell::Ref<'_, (std::option::Option<i32>, i32)>`
ここから cell.borrow() が3行目でdropされるだろうという当初の思いに反し、ずいぶん長生きしていることがわかる。
The Rust Reference の一時スコープの節のNotesによると、match式の検査対象(scrutinee)にある一時変数(temporary)はmatch式の評価中はdropされない。
また先にあげたReferenceによると、一時変数が生存する一時スコープとなるのは以下のうち最小のスコープである。
- 関数全体
- 文
- if, while, loop式の本体
- if式のelseブロック
- if, while式の条件式やmatchのガード
- matchのアームにある式
- 短絡評価される論理演算子の
第2引数
以上を踏まえると、if-let式はmatch式と等価であるから、問題のコードのふるまいとセミコロンを取った時のエラーについて説明がつく。
セミコロンなし(if-let式がmain関数の返り値として扱われる)だと cell.borrow() の生存するスコープはmain関数全体となり、cell よりも後にdropされることになるためコンパイラがエラーを出す。
セミコロンをつけると cell.borrow() はmain関数が終わる前にdropされるためコンパイラは何も言わないが、4行目ではまだ生きているので可変参照を得るのに失敗する。
解決方法にあげたコードでは、cell.borrow() がlet文内にあり3行目でdropされるので可変参照を得られている。
Rust 1.67.0 リリースに伴う追記(2023.01.27)
1.67.0において、Remove drop order twist of && and || and make them associative がマージされた。 この変更により、それまでランタイムエラー(BorrowMutError)になっていた以下のコードが動くようになった。
fn main() { let cell = core::cell::RefCell::new((true, false)); let _ = cell.borrow_mut().0 && cell.borrow_mut().1; }
これは&&や||の第1引数も一時スコープとなったということだから、上記リストの最後の項目に取り消し線を入れた。
[おまけ]let-elseの場合
本件の調査の過程で、1.65.0で安定化されたlet-else文はif-let式と似たことができるものの異なる挙動を示すことに気付いた。
まずif-let式の方だが、先に述べた理由から以下のコードはパニックする。
fn main() { let cell = core::cell::RefCell::new((Some(42), 0)); if let Some(0) = cell.borrow().0 { } else { cell.borrow_mut().1 = 42; // thread 'main' panicked at 'already borrowed: BorrowMutError', src\main.rs:5:14 }; }
一方でlet-else文で同じようなことをしてもパニックしない。
fn main() { let cell = core::cell::RefCell::new((Some(42), 0)); let Some(0) = cell.borrow().0 else { cell.borrow_mut().1 = 42; return; }; }
この挙動はRFCとは異なる(本記事執筆時点)ので混乱したが、 let else 安定化のPull Requestによれば、マッチしなかった場合、一時変数はelseブロックの前でdropされるようにRFCから変更されたとのことである。 このdropタイミングになった理由は、マッチしたときはlet文と同じ挙動にするためとelseブロックでの処理のためであるように見受けられる。
まとめ
メソッドや関数の呼び出しにより生成された構造体が式の評価後も一時変数として生き続けることがある。 特にscrutineeにあたる式の途中で生成されたものは注意が必要である。 これによる一時変数の予期せぬ長生きは、変数への代入を含む文(let文や代入式+セミコロン)を用いて一時スコープを小さくすることで防ぐことができる。
タグ「Rust」の記事
次の記事
2022.12.11 作成
クロージャの引数の参照型を推論させるとHRTBが導入されないことがある
最新記事
2022.12.11 作成
クロージャの引数の参照型を推論させるとHRTBが導入されないことがある