LoginSignup
7
0

More than 1 year has passed since last update.

DropとGuard, MaybeUninit

Last updated at Posted at 2022-12-17

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では、0x010x00だけのバイトである、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);なのですが。

終わりに

そんな感じでDropMaybeUninitを紹介する傍らでGuardと言われるパターンを見ていきました。Rustはunsafeでもなるべく安全に書けるようになっています。まあunsafeは極力触らない方がいいのですが。

  1. 実際はstdには安全なラッパーが用意されていて、safe Rustの範囲内で実装できることが増えてきた。1.63 からのstd::array::from_fnなど便利になってきた。

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0