Day 4: スマートポインタ実践 - 解説

目次

通常のポインタとの比較

// C言語: 生ポインタ(危険)
int* ptr = malloc(sizeof(int));
*ptr = 42;
// free を忘れるとメモリリーク!

// Rust: Box(安全)
let ptr = Box::new(42);
// スコープを抜けると自動的に解放される

Rustの優位性:

  • コンパイル時に所有権を検証
  • ダブルフリーの防止
  • Use-after-freeの防止

なぜスマートポインタが必要か

手動メモリ管理の主な問題を解決するためです:

問題1: メモリリーク

// C言語: メモリリーク
struct Node* create_node() {
    struct Node* node = malloc(sizeof(struct Node));
    node->value = 42;
    return node;  // freeを忘れたら?
}

// Rust: 自動解放
fn create_node() -> Box<Node> {
    Box::new(Node { value: 42 })
    // 自動的にメモリ解放
}

問題2: ダブルフリー

// C言語: ダブルフリー(危険)
free(ptr);
free(ptr);  // クラッシュ!

// Rust: コンパイルエラー
let ptr = Box::new(42);
drop(ptr);
// drop(ptr);  // コンパイルエラー!値は既にムーブされている

問題3: Use-after-free

// C言語: Use-after-free
free(ptr);
printf("%d", *ptr);  // 未定義動作!

// Rust: コンパイルエラー
let ptr = Box::new(42);
drop(ptr);
// println!("{}", *ptr);  // コンパイルエラー!

設計原則

RAII (Resource Acquisition Is Initialization)

スマートポインタはRAII原則に基づいています:

// リソースの取得 = 初期化
let file = File::open("data.txt")?;

// スコープを抜けると自動的に解放
// Dropトレイトが呼ばれる

メモリ以外のリソースにも適用:

  • ファイルハンドル
  • ネットワーク接続
  • データベース接続
  • ロック

Deref トレイト: スマートポインタの核心

Derefトレイトにより、スマートポインタは通常のポインタのように振る舞います:

use std::ops::Deref;

struct MyBox<T>(T);

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

// 使用例
let x = MyBox(5);
assert_eq!(5, *x);  // Derefで自動変換

Deref強制(Deref Coercion):

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

let m = MyBox(String::from("Rust"));
hello(&m);  // &MyBox<String> -> &String -> &str

Drop トレイト: 自動クリーンアップ

Dropトレイトにより、スコープを抜ける際に自動的にクリーンアップが行われます:

struct CustomPointer {
    data: String,
}

impl Drop for CustomPointer {
    fn drop(&mut self) {
        println!("Dropping CustomPointer with data `{}`!", self.data);
    }
}

fn main() {
    let _c = CustomPointer {
        data: String::from("data"),
    };
    println!("CustomPointer created.");
}
// スコープを抜けるとdropが自動実行
// 出力:
// CustomPointer created.
// Dropping CustomPointer with data `data`!

Interior Mutability(内部可変性)

RefCellは「内部可変性」パターンを実現します:

// 通常の借用ルール(コンパイル時チェック)
let mut x = 5;
let y = &mut x;  // OK
// let z = &mut x;  // エラー!

// 内部可変性(実行時チェック)
let x = RefCell::new(5);
let y = x.borrow_mut();  // OK
// let z = x.borrow_mut();  // パニック!(実行時)

トレードオフ:

  • 利点: 柔軟性の向上
  • 欠点: 実行時オーバーヘッド

メンタルモデル

Box: ヒープへの所有権

Boxを「ヒープ上のデータへの唯一の所有権」として理解します。

// スタック         ヒープ
// ┌────┐          ┌─────────┐
// │ x  │─────────>│  42     │
// └────┘          └─────────┘
let x = Box::new(42);

// xがドロップされると、ヒープメモリも解放

使用例:

  • 大きなデータのスタック溢れ防止
  • 再帰的なデータ構造
  • トレイトオブジェクト

Rc: 共有所有権のグラフ

Rcを「参照カウント付きのグラフノード」として理解します。

//     ┌──────────────┐
//     │  Rc<Vec>     │
//     │  count: 3    │
//     │  [1, 2, 3]   │
//     └──────────────┘
//        ↑   ↑   ↑
//        │   │   │
//        a   b   c

let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a);
let c = Rc::clone(&a);

ライフサイクル:

  • Rc::new(): カウント = 1
  • Rc::clone(): カウント +1
  • drop(): カウント -1
  • カウント = 0: メモリ解放

Arc: スレッド安全な共有

Arcを「アトミック操作による安全な共有」として理解します。

use std::sync::Arc;
use std::thread;

let data = Arc::new(vec![1, 2, 3]);

// スレッド1        スレッド2
//    ↓               ↓
//    └───→ Arc ←────┘
//          ↓
//       [1, 2, 3]

let data1 = Arc::clone(&data);
thread::spawn(move || {
    println!("{:?}", data1);
});

アトミック操作:

  • カウンタの増減がスレッド安全
  • データ競合の防止

RefCell: 実行時借用チェック

RefCellを「実行時に動的にチェックする借用」として理解します。

//  コンパイル時         実行時
//      ↓                 ↓
//   RefCell<T>    borrow() / borrow_mut()
//                        ↓
//                  実行時チェック
//                        ↓
//                  OK / パニック

let data = RefCell::new(5);

{
    let r1 = data.borrow();     // OK: カウント +1
    let r2 = data.borrow();     // OK: カウント +1
} // カウント = 0

{
    let mut w = data.borrow_mut();  // OK: 排他的借用
    *w += 1;
}

ビジュアル図解

Box のメモリレイアウト

関数スタック
┌─────────────────────────────┐
│ main()                      │
│ ┌─────────────────────────┐ │
│ │ list: Box<List>         │ │
│ │ ┌─────┐                 │ │
│ │ │ ptr │─┐               │ │
│ │ └─────┘ │               │ │
│ └─────────┼───────────────┘ │
└───────────┼─────────────────┘
            │
            ↓
ヒープ
┌───────────────────────────────┐
│ Cons(1, Box<List>)            │
│ ┌───┬─────┐                   │
│ │ 1 │ ptr │─┐                 │
│ └───┴─────┘ │                 │
│             ↓                 │
│   Cons(2, Box<List>)          │
│   ┌───┬─────┐                 │
│   │ 2 │ ptr │─┐               │
│   └───┴─────┘ │               │
│               ↓               │
│     Cons(3, Box<Nil>)         │
│     ┌───┬─────┐               │
│     │ 3 │ Nil │               │
│     └───┴─────┘               │
└───────────────────────────────┘

特徴:

  • 各ノードがヒープに配置
  • Box がドロップされると、再帰的にすべて解放

Rc の参照カウント

初期状態: count = 1
┌────────────────┐
│  Rc<Vec>       │
│  count: 1      │
│  [1, 2, 3]     │
└────────────────┘
      ↑
      │
      a

Rc::clone後: count = 2
┌────────────────┐
│  Rc<Vec>       │
│  count: 2      │
│  [1, 2, 3]     │
└────────────────┘
      ↑     ↑
      │     │
      a     b

さらにclone: count = 3
┌────────────────┐
│  Rc<Vec>       │
│  count: 3      │
│  [1, 2, 3]     │
└────────────────┘
   ↑     ↑     ↑
   │     │     │
   a     b     c

bドロップ: count = 2
┌────────────────┐
│  Rc<Vec>       │
│  count: 2      │
│  [1, 2, 3]     │
└────────────────┘
   ↑           ↑
   │           │
   a           c

すべてドロップ: count = 0 → メモリ解放

Rc> のグラフ構造

グラフ構造の例(双方向):

Node 1                     Node 2
┌─────────────────────┐   ┌─────────────────────┐
│ value: 1            │   │ value: 2            │
│ neighbors:          │   │ neighbors:          │
│   RefCell<Vec<Rc>>  │   │   RefCell<Vec<Rc>>  │
│   ┌──────────────┐  │   │   ┌──────────────┐  │
│   │ [Rc→Node2] ──┼──┼───┼──>│              │  │
│   └──────────────┘  │   │   └──────────────┘  │
└─────────────────────┘   └─────────────────────┘
         ↑                         │
         │                         │
         └─────────────────────────┘
              Rc→Node1

参照カウント:
- Node 1: 2 (自身 + Node2の隣接リスト)
- Node 2: 2 (自身 + Node1の隣接リスト)

RefCell の借用チェック

RefCell内部の状態機械:

初期状態: BorrowState::Unused
┌────────────────────┐
│ RefCell<i32>       │
│ state: Unused      │
│ value: 5           │
└────────────────────┘

borrow() → BorrowState::Shared(1)
┌────────────────────┐
│ RefCell<i32>       │
│ state: Shared(1)   │  ← 不変参照の数
│ value: 5           │
└────────────────────┘

さらにborrow() → Shared(2)
┌────────────────────┐
│ RefCell<i32>       │
│ state: Shared(2)   │
│ value: 5           │
└────────────────────┘

すべて解放 → Unused
┌────────────────────┐
│ RefCell<i32>       │
│ state: Unused      │
│ value: 5           │
└────────────────────┘

borrow_mut() → BorrowState::Exclusive
┌────────────────────┐
│ RefCell<i32>       │
│ state: Exclusive   │  ← 可変参照
│ value: 6           │  ← 変更可能
└────────────────────┘

重要なポイント

1. スマートポインタは型システムの一部

// これらは異なる型
let box_ptr: Box<i32> = Box::new(5);
let rc_ptr: Rc<i32> = Rc::new(5);
let arc_ptr: Arc<i32> = Arc::new(5);

// コンパイラが型の違いを強制
// box_ptr = rc_ptr;  // エラー!型が異なる

意味:

  • 所有権の意味論が型に現れる
  • コンパイル時に正しい使い方を強制

2. ゼロコスト抽象化(一部例外あり)

// Box: ほぼゼロコスト
let x = Box::new(42);
// アセンブリ: 単なるポインタアクセス

// Rc/Arc: カウンタのオーバーヘッド
let x = Rc::new(42);
let y = Rc::clone(&x);  // カウンタ +1 (小さなコスト)

コスト比較:

アロケーション クローン ドロップ
Box malloc相当 不可 free相当
Rc malloc + カウンタ カウンタ+1 カウンタ-1
Arc malloc + アトミック アトミック+1 アトミック-1

3. 内部可変性のトレードオフ

// コンパイル時チェック(高速、安全)
let mut x = 5;
let y = &mut x;

// 実行時チェック(柔軟、小コスト)
let x = RefCell::new(5);
let y = x.borrow_mut();

選択基準:

  • コンパイル時に関係が確定 → 通常の借用
  • 実行時に動的に決まる → RefCell

4. 循環参照に注意

use std::rc::{Rc, Weak};

// 悪い例: 循環参照でメモリリーク
struct Node {
    parent: Rc<Node>,      // ❌
    children: Vec<Rc<Node>>,
}

// 良い例: Weakで循環を断ち切る
struct Node {
    parent: Weak<Node>,    // ✅
    children: Vec<Rc<Node>>,
}

ルール:

  • 親→子: Rc (強い参照)
  • 子→親: Weak (弱い参照)

セルフチェック質問

質問1: Box の必要性

Q: 次のコードでBoxが必要な理由は?

enum List {
    Cons(i32, Box<List>),
    Nil,
}

答えを見る

A: 再帰的な型のサイズを確定させるため。

理由: Boxなしの場合:

enum List {
    Cons(i32, List),  // エラー!
    Nil,
}
// Listのサイズ = i32のサイズ + Listのサイズ + ...
// → 無限サイズ!

Boxありの場合:

enum List {
    Cons(i32, Box<List>),  // OK
    Nil,
}
// Listのサイズ = max(i32 + ポインタサイズ, 0)
// → 確定サイズ!

メモリレイアウト:

List = 最大(
  Cons(4 bytes + 8 bytes),  // i32 + Box
  Nil(0 bytes)
) = 12 bytes (+ padding)

質問2: Rc vs Arc

Q: いつRcを使い、いつArcを使うべきですか?

答えを見る

A:

  • Rc: シングルスレッドの共有所有権
  • Arc: マルチスレッドの共有所有権

判断基準:

use std::thread;

// Rc: シングルスレッド専用
let data = Rc::new(vec![1, 2, 3]);
// thread::spawn(move || println!("{:?}", data));  // エラー!

// Arc: スレッド間で共有可能
let data = Arc::new(vec![1, 2, 3]);
thread::spawn(move || println!("{:?}", data));  // OK

パフォーマンス:

  • Rc: 高速(非アトミック操作)
  • Arc: やや遅い(アトミック操作)

ルール: > スレッドを使わないなら Rc、スレッドで共有するなら Arc

質問3: RefCell のパニック

Q: 次のコードはパニックしますか?

let data = RefCell::new(5);
let r1 = data.borrow();
let r2 = data.borrow();
drop(r1);
let mut w = data.borrow_mut();

答えを見る

A: いいえ、パニックしません。

理由:

let data = RefCell::new(5);
let r1 = data.borrow();     // Shared(1)
let r2 = data.borrow();     // Shared(2)
drop(r1);                   // Shared(1)
// ここでr2がまだ存在する
// let mut w = data.borrow_mut();  // パニック!

修正:

let data = RefCell::new(5);
{
    let r1 = data.borrow();
    let r2 = data.borrow();
}  // r1, r2 がドロップ

let mut w = data.borrow_mut();  // OK

ルール: > すべての不変参照がドロップされた後でのみ、可変参照を取得可能

質問4: 循環参照の検出

Q: 次のコードでメモリリークは発生しますか?

use std::rc::Rc;
use std::cell::RefCell;

struct Node {
    value: i32,
    next: RefCell<Option<Rc<Node>>>,
}

let node1 = Rc::new(Node { value: 1, next: RefCell::new(None) });
let node2 = Rc::new(Node { value: 2, next: RefCell::new(None) });

*node1.next.borrow_mut() = Some(Rc::clone(&node2));
*node2.next.borrow_mut() = Some(Rc::clone(&node1));

答えを見る

A: はい、メモリリークが発生します。

理由:

node1 ───> Node(1)
  ↑          │
  │          ↓
  │       next: Some(Rc→node2)
  │
  └─────────┐
node2       │
  ↓         │
Node(2)     │
  │         │
  ↓         │
next: Some(Rc→node1)

参照カウント:

  • node1: 2 (自身 + node2のnext)
  • node2: 2 (自身 + node1のnext)

問題:

  • node1とnode2がスコープを抜けてもカウントは1のまま
  • 永遠に解放されない!

解決策:

use std::rc::{Rc, Weak};

struct Node {
    value: i32,
    next: RefCell<Option<Weak<Node>>>,  // Weakを使う
}

質問5: スマートポインタの選択

Q: 次の状況でどのスマートポインタを使うべきですか?

  • DOMツリー(親子双方向参照)
  • 設定ファイルの共有(読み取り専用、複数スレッド)
  • キャッシュ(複数所有者、時々変更、シングルスレッド)

答えを見る

A:

  • DOMツリー: Rc> + Weak
   struct Node {
       parent: RefCell<Weak<Node>>,      // 弱参照で循環回避
       children: RefCell<Vec<Rc<Node>>>, // 強参照
   }
   

  • 設定ファイル: Arc
   let config = Arc::new(load_config());
   // 複数スレッドで共有
   

  • キャッシュ: Rc>>
   let cache = Rc::new(RefCell::new(HashMap::new()));
   // 複数の所有者、時々変更
   

判断フローチャート:

複数所有者?
├─ No → Box<T>
└─ Yes
   ├─ マルチスレッド?
   │  ├─ No → Rc<T>
   │  └─ Yes → Arc<T>
   └─ 変更必要?
      ├─ No → そのまま
      └─ Yes
         ├─ シングルスレッド → RefCell<T>
         └─ マルチスレッド → Mutex<T> or RwLock<T>

まとめ

スマートポインタの解説を通じて、以下の概念を学びました:

  • 本質: 所有権を管理する型
  • 設計原則: RAII、Deref、Drop、内部可変性
  • メンタルモデル: 所有権のグラフ、参照カウント、実行時チェック
  • ビジュアル: メモリレイアウト、参照関係
  • 重要ポイント: 型の選択、パフォーマンス、循環参照

これらの理解を基に、実践的な課題に取り組み、Rustの高度なメモリ管理パターンを習得してください。スマートポインタをマスターすることで、安全で効率的なRustコードが書けるようになります。