5 分で読了 #rust #所有と借用
このページの目次

所有権モデル

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つのルール:

  1. Rust では、各値には所有者(owner)が1つだけ存在する
  2. 所有者がスコープを抜けると、値は drop される
  3. 代入・引数渡し・戻り値の返却は所有権を移動(move)する(ただし、型が Copy を実装している場合は例外)

Move セマンティクス

let s1 = String::from("hello");
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!("{} {}", x, y);             // OK

Copy トレイトはコンパイラが自動的に認識するマーカートレイトです——Copy を実装している型は、代入時に move ではなくビットコピーが行われます。条件は、型のすべてのフィールドが Copy を実装していることです。i32boolchar&T は Copy を実装しています;StringVec<T>Box<T> は実装していません——これらはヒープデータを持つからです。

Clone明示的なディープコピーです:.clone() はプログラマが呼び出すものであり、新しいヒープメモリを割り当てる可能性があります。StringClone を実装していますが Copy は実装していません——明示的な .clone() は新しい String を生成し、所有権の移動は発生しません。

let s1 = String::from("hello");
let s2 = s1.clone();                 // ディープコピー: 新しいヒープ割り当て
println!("{} {}", s1, s2);           // 2つの独立した String が有効

Move が発生する場所

// 1. 代入
let s2 = s1;                         // move

// 2. 引数渡し
fn take(s: String) {}                // s が所有権を取得
take(s1);                            // s1 が s に move される → s1 は無効化

// 3. 戻り値
fn give() -> String { String::from("hi") }  // 所有権は戻り値とともに呼び出し元に移動

// 4. パターンマッチ
let (a, b) = (vec![1], vec![2]);     // 2つの Vec がそれぞれ a と b に move される

// 5. クロージャによるキャプチャ
let v = vec![1, 2, 3];
let closure = || { println!("{:?}", v); };  // クロージャは 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 が実装されている場合、解放すべきリソースがあることを意味するため、単純なビットコピーには適さない)
#[derive(Copy, Clone)]               // 手動で Copy を derive (Clone も必要)
struct Point { x: i32, y: i32 }      // すべてのフィールドが Copy → Point も Copy

struct MyString(String);             // String は Copy 不可 → MyString も Copy 不可
// MyString に対して Copy を derive することはできない!

部分移動 (Partial Move)

let p = (String::from("hello"), 42);
let (s, n) = p;                      // s が String の所有権を取得、n は 42 をコピー
// println!("{:?}", p);             // ERROR: p.0 は既に move されている
println!("{}", p.1);                 // OK: p.1 は i32 で、コピーされており、p の中に残っている

Drop の順序

スコープ終了時、変数は宣言の逆順で drop されます:

{
    let a = String::from("first");   // 先に宣言
    let b = String::from("second");  // 後に宣言
}                                    // 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