このページの目次
所有権モデル
Rust の所有権モデルには3つのルールしかありません: 各値には一意の所有者が1人だけいる、所有者がスコープを抜けると値は drop される、代入と引数渡しは所有権を移動(move)する。Copy は move の例外(ビットコピー後、2つの変数がともに使用可能)、Clone は明示的なディープコピー、partial move を使うと構造体から一部のフィールドだけを移動できる。この3つのルールは、Rust が GC なしでメモリ安全性を保証できる基盤である。
なぜ Rust には所有権が必要なのか
C の手動 malloc/free と C++ の RAII は、プログラマの規律に依存しています——解放の忘れ、ダブルフリー、use-after-free はコンパイル時に全く検出できません。GC 言語は自動管理しますが、ランタイムのオーバーヘッドと stop-the-world の遅延を導入します。Rust の答えは:コンパイル時に所有権ルールによってメモリ安全性を保証し、ランタイムのオーバーヘッドはゼロです。
核心的な3つのルール:
- Rust では、各値には所有者(owner)が1つだけ存在する
- 所有者がスコープを抜けると、値は drop される
- 代入・引数渡し・戻り値の返却は所有権を移動(move)する(ただし、型が Copy を実装している場合は例外)
Move セマンティクス
let s1 = Stringfrom;
let s2 = s1; // s1 の所有権が s2 に移動
// println!("{}", s1); // ERROR: s1 は既に move されているため、使用できない
String はスタック上に3つのワード(ポインタ、長さ、容量)を保持し、実際のデータはヒープ上にあります。let s2 = s1 はシャローコピー(スタック上の3つのワードをコピー)を行い、その後 s1 を無効化します——ダブルフリーは発生しません。なぜなら、ヒープメモリの解放はスコープ終了時に s2 だけが drop を呼び出すからです。
コンパイラが実際に生成する擬似コード:
s1 = String { ptr → heap[5 bytes "hello"], len: 5, cap: 5 }
s2 = String { ptr → 同じヒープ, len: 5, cap: 5 } // move: スタック上のフィールドをコピー
s1 は "moved out" としてマークされる → drop(s1) はコンパイラによってスキップされる
Copy と Clone
let x = 42;
let y = x; // x はまだ使用可能 — i32 は Copy を実装している
println!; // OK
Copy トレイトはコンパイラが自動的に認識するマーカートレイトです——Copy を実装している型は、代入時に move ではなくビットコピーが行われます。条件は、型のすべてのフィールドが Copy を実装していることです。i32、bool、char、&T は Copy を実装しています;String、Vec<T>、Box<T> は実装していません——これらはヒープデータを持つからです。
Clone は明示的なディープコピーです:.clone() はプログラマが呼び出すものであり、新しいヒープメモリを割り当てる可能性があります。String は Clone を実装していますが Copy は実装していません——明示的な .clone() は新しい String を生成し、所有権の移動は発生しません。
let s1 = Stringfrom;
let s2 = s1.clone; // ディープコピー: 新しいヒープ割り当て
println!; // 2つの独立した String が有効
Move が発生する場所
// 1. 代入
let s2 = s1; // move
// 2. 引数渡し
// s が所有権を取得
take; // s1 が s に move される → s1 は無効化
// 3. 戻り値
// 所有権は戻り値とともに呼び出し元に移動
// 4. パターンマッチ
let = ; // 2つの Vec がそれぞれ a と b に move される
// 5. クロージャによるキャプチャ
let v = vec!;
let closure = ; // クロージャは v を move してキャプチャ (Vec は Copy を実装していないため)
// v は外部で使用できなくなる!
スタックデータ vs ヒープデータ
move と copy を理解するための鍵:スタック上のデータはビットコピーが安全——所有権の移動はスタック上の内容のコピーだけで済みます。ヒープデータの所有権移動も、スタック上のポインタのコピーだけで済み、ヒープ上のデータはコピーされません。
let s1 = String::from("hello"); // stack: [ptr|len|cap], heap: "hello"
let s2 = s1; // stack copy: [同じ ptr|同じ len|同じ cap]
// s1 は moved としてマーク → drop(s1) は呼び出されない
// スコープ終了: drop(s2) → ヒープ上の "hello" を解放
Copy の条件
どのような型が自動的に Copy を実装しているのでしょうか?
- すべてのフィールドが Copy を実装している
- Drop を実装していない(Drop が実装されている場合、解放すべきリソースがあることを意味するため、単純なビットコピーには適さない)
// 手動で Copy を derive (Clone も必要)
// すべてのフィールドが Copy → Point も Copy
; // String は Copy 不可 → MyString も Copy 不可
// MyString に対して Copy を derive することはできない!
部分移動 (Partial Move)
let p = ;
let = p; // s が String の所有権を取得、n は 42 をコピー
// println!("{:?}", p); // ERROR: p.0 は既に move されている
println!; // OK: p.1 は i32 で、コピーされており、p の中に残っている
Drop の順序
スコープ終了時、変数は宣言の逆順で drop されます:
// drop(b) → drop(a)
これは安全性にとって重要です——後続の変数が先行変数を参照している可能性があるため、後続の変数を先に drop することで、参照が常に有効であることを保証します。
参考
- Rust Book: Chapter 4.1 — What is Ownership
- Rust Reference: destructors, move semantics
- RFC: RFC 213 (default Move semantics)
Keywords: 所有権, move, Copy, Clone, stack, heap, drop, partial move, RAII