Dropとは
RustはRAIIの強制と所有権を用いてリソースの管理をしています。
このリソース管理の一端を担っている仕組みとしてDrop
があります。
コンパイラは、所有権が消えたと判断した場所に自動的にリソースの解放するコードを挿入してくれます。
例えばstructならフィールドを上から解放していきます。
{
let x = Box::new(5);
println!("{x}");
//ここでxは開放される
}
ただ解放する際に追加で処理したいときがあります。
ポインタで持っているデータを解放したりとか、解放したことをどこかに通知したいなどです。そういう時にはDrop
を使います。
たとえばVec
はアロケートされているデータをすべて開放したり、Arc
では参照カウンタを減らし、参照カウンタが0になったらリソースを開放するということを行っています。
まあ他の言語でいうデストラクタですね。
Rustでライブラリを書いたりしてリソースの管理をしない限り触る必要はあんまりなく意識されないtraitです。
ただ少しパフォーマンスを意識したりしてunsafe Rustに入り始めたときには必要になってくるものです。その際の一つのパターンを紹介します。
Guard
Guardといってもそういったtraitがあるわけでもなく、そういった名前のstructで実装することが多いからこの名前でとってきました。
このGuardは何のために用いるかというと、リソースの初期化中に初期化に失敗したときにリソースを安全に解放するためです。
Rustは所有権を解放したときにdropされるはずで、panicしたとしても実行されるはずですが、どうしてこのGuardが必要になるのでしょうか?
それはdropされない型があるからです。
ポインタ型はそのポインタが指す相手は解放しませんし、
またunion型もそのデータは解放されません(というよりDropする型は入れれません)。
そしてunionの中で最も使われるのがMaybeUninit
です。
MaybeUninit
例えば初期化したいとしましょう。
メモリ領域を確保したとして、Rustではその領域のデータが型としての制約に従っているようにしないといけません。
(bool
では、0x01
と0x00
だけのバイトである、str
はUtf-8に従っているなど)。
しかし、Arrayなど生成時にはすべてのデータが規約に従っている状態で作れない場合があります。1
この時にMaybeUninit
を用いることでまだ未初期化であると宣言することができます。
そして初期化が終わったときに通常の型に戻せます。(もちろんunsafeです、コンパイラが初期化したかを保証できないので。)
let mut uninit = MaybeUninit::uninit();
uninit.write(5_i32);
let init = unsafe { uninit.assume_init() };
上のようにMaybeUninit
で初期化した後にassume_init
で通常の型に戻すのですが、戻す前にpanicしたらどうなるでしょうか?
let init = "bbbb".to_owned();
let mut uninit = MaybeUninit::uninit();
uninit.write("aaaa".to_owned());
panic!();
// initのStringは解放される。
//uninitに残ったStringは解放されない!
dropされない型なので確保されたリソースが解放されません。メモリリークになります!
panicしないときでも例えばResult
を返す型で早くに返したいけどしっかりとリソースの解放をしたいときには困ります。
再びGuardに戻って
このようにpanicしたときにメモリリークを防ぐのはGuardパターンです。
https://doc.rust-lang.org/src/core/array/mod.rs.html#804
などのような関数の内側で実装されています。
上の例だとGuardは以下のように実装されています。
struct Guard<'a, T, const N: usize> {
array_mut: &'a mut [MaybeUninit<T>; N],
initialized: usize,
}
impl<T, const N: usize> Drop for Guard<'_, T, N> {
fn drop(&mut self) {
debug_assert!(self.initialized <= N);
// SAFETY: this slice will contain only initialized objects.
unsafe {
crate::ptr::drop_in_place(MaybeUninit::slice_assume_init_mut(
&mut self.array_mut.get_unchecked_mut(..self.initialized),
));
}
}
}
これはGuard型が不意にDrop
するときにそれまで初期化していた範囲をしっかりと解放するように用いられています。
初期化時の変更をしていくたびにinitialized
を更新していきます。
そして最終的には中の参照先のリソースをassume_init
することで、リソースが初期化済みであることをしめして、Guard型をforget
して解放が不要であることを知らせます。(これを忘れると誤って解放されることになります。)
let mut array = MaybeUninit::uninit_array::<N>();
let mut guard = Guard { array_mut: &mut array, initialized: 0 };
//arrayを初期化する
//ここで落ちるとGuardがdropしてリソースが解放される。
mem::forget(guard);// ここが重要 forgetをするとDropが呼ばれなくなる。
let output = unsafe { MaybeUninit::array_assume_init(array) };
このforget
はDropの対象から外すことになります。
ただこのforget
はいろいろ危険なのにunsafeでないので気を付けてください…。
基本的にはforget
を使わずにManuallyDrop
を使うといいとされています。
まあforget
の中身はlet _ = ManuallyDrop::new(t);
なのですが。
終わりに
そんな感じでDrop
とMaybeUninit
を紹介する傍らでGuardと言われるパターンを見ていきました。Rustはunsafeでもなるべく安全に書けるようになっています。まあunsafeは極力触らない方がいいのですが。
-
実際はstdには安全なラッパーが用意されていて、safe Rustの範囲内で実装できることが増えてきた。1.63 からのstd::array::from_fnなど便利になってきた。 ↩