🔥

Rust スマートポインタ 整理

2022/11/06に公開

Rust スマートポインタ 整理

そもそもスマートポインタについて

スマートポインタはRustに限った概念ではなくC++とかでも見られる。(というかC++が発端?)
一般的なポインタに、メタデータとか何かしら処理とかが付与された概念。

例えば

参照カウンタ方式のスマートポインタ。
ポインタに追加で、「参照カウンタ」と「参照の所有者がいなくなった(参照カウンタが0になった)タイミングでポインタの指す先のポインタを開放する」能力が付与されている。

Rustにおけるスマートポインタ

一般的には構造体を使用して実装されている。
構造体にDerefトレイトとDropトレイトを実装することで、実質スマートポインタとして振る舞ってもらえるようになる。
Rustにおけるメモリ管理において、よく使われる一般的なデザインパターンらしい。
ちなみに、よく使うStringVec<T>もスマートポインタ。

Derefトレイト

構造体の変数に対して、参照外しを行った際の挙動を実装できる。
スマートポインタの構造体に対して参照外ししたときに、構造体内に持つポインタの参照先を返すように書くことができる。

// Derefトレイト実装の例
use std::ops::Deref;

struct DerefExample<T> {
    value: T
}

// Derefトレイトを実装
impl<T> Deref for DerefExample<T> {
    type Target = T;

    // この構造体の変数に'*'をつけて参照外しした際の処理
    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

let x = DerefExample { value: 'a' };
assert_eq!('a', *x);  // *xで'a'が返ってくる

Dropトレイト

構造体の変数が、スコープを抜ける際の処理を実装できる。
スマートポインタの構造体がスコープを抜ける際に、構造体内に持つポインタの参照先を、ある条件に応じて開放したりとか書くことができる。

// Dropトレイとの実装例
struct HasDrop;

// Dropトレイトを実装
impl Drop for HasDrop {
    // この構造体の変数がスコープを抜ける際の処理
    fn drop(&mut self) {
        println!("Dropping HasDrop!");
    }
}

struct HasTwoDrops {
    one: HasDrop,
    two: HasDrop,
}

impl Drop for HasTwoDrops {
    fn drop(&mut self) {
        println!("Dropping HasTwoDrops!");
    }
}

fn main() {
    let _x = HasTwoDrops { one: HasDrop, two: HasDrop };
    println!("Running!");
}

// 出力
// Running!
// Dropping HasTwoDrops!
// Dropping HasDrop!
// Dropping HasDrop!

Box<T>

最も単純で一般的なスマートポインタ。
実際のデータはヒープ上に格納され、構造体内にはヒープ上のデータへのポインタを保持する。
Derefトレイトを実装しているので参照外しすることでヒープ上の値を取得できるし、Dropトレイトが実装されているのでスコープを抜けるタイミングでヒープ上のデータも開放される。

ユースケースとしては一般的なヒープと対して変わらず、以下のような感じ。

  • コンパイル時にサイズが確定しないような値を使用する場合
  • 大きな値を扱う際にデータのコピーが発生させたくない場合

Rust特有の事情としては以下。

  • ある特定の型ではなく、あるトレイトを実装した型として扱いたい場合
    トレイトオブジェクトとかいうやつ
    (クラスベースのオブジェクト指向で言うと、あるクラスやインターフェースを継承しているクラスを受け入れたい場合など)

Rc<T>/Arc<T>

いわゆる、参照カウント方式のスマートポインタ。Reference Countingの省略形。
Box<T>同様ヒープ上に値は確保され、構造体にはヒープ上のデータへのポインタに加え、参照カウンタ(データを参照している変数の数)も保持する。

複数ヶ所から参照され、参照している変数がいなくなったタイミングで開放したい場合に使える。
ユースケースとしては以下。

  • どの変数が最後にデータを使用し終わるか、コンパイル時に決定できない場合
    例えば、複数の構造体で状態を共有している場合など、状態を参照されている間は開放されてほしくないとか

ちなみにRcはスレッドセーフではないので、マルチスレッドな処理などで使いたい場合はArc (Atomically Reference Counted)を使うらしい。

RefCell<T>

RefCell<T>の特性を知る前に、「内部可変性」について知っとく。

内部可変性

データへの参照が不変参照である時であっても、データを変更可能であるRustのデザインパターン。
一般的に、Rustにおける借用規則によってこのような操作はコンパイル時に弾かれるものの、一時的にunsafeを利用して可能にしている。

RefCell<T>の借用チェック

普通の参照やBox<T>がコンパイル時に借用規則をチェックされるのに対し、RefCell<T>では実行時に借用チェックが行われるらしい。
これらの値に対しての参照が不変でも、内部の値が変更できる。
Rc<T>と違って所有者は単一だが、組み合わせることで「可変なデータに複数の所有者を持たせる」などができる。

最後に

他にも、Cell<T>Mutex<T>なんかもあるらしいけど別途調べたい。

Discussion