本文へスキップ
BecomeCoder

Rustコース · 第5章 参照と借用 · レッスン22

ダングリング参照 ― コンパイラが未然に防ぐ

ローカル実施

導入

C言語の悪名高いバグに「ダングリングポインタ」があります。すでに解放されたメモリを指し続けるポインタで、それを使うと解放後の領域を触ってしまい、深刻なバグになります。Rust は、この危険なコードをそもそもコンパイルさせません

説明

「解放済みのデータを指す参照」を作ろうとする、危ないコードを考えます。

fn main() {
    let ref_to_nothing = dangle();
}

fn dangle() -> &String {          // String への参照を返そうとする
    let s = String::from("hello"); // s は関数内でヒープを確保
    &s                             // s への参照を返す……
}   // ← ここで s がスコープを抜け、ヒープが解放される!
    //    返した参照は「解放済みのデータ」を指すことになる → 危険

dangle の中で作った s は、関数が終わるとスコープを抜けて解放されます(所有権ルール3)。なのに &ss への参照)を返してしまうと、呼び出し側はすでに解放されたヒープを指す参照を受け取ることになります。これがダングリング参照です。

C言語ならコンパイルが通ってしまい、実行時に運が悪ければクラッシュ、運が良い(悪い)と一見動いてしまってセキュリティホールに——という悪夢のバグです。

Rust のコンパイラは、これをコンパイル時に検出して拒否します。

error[E0106]: missing lifetime specifier
  --> src/main.rs
   |   fn dangle() -> &String {
   |                  ^ expected named lifetime parameter
   = help: this function's return type contains a borrowed value,
           but there is no value for it to be borrowed from

コンパイラは「借用した値を返そうとしているが、その借用元がどこにも残らない(=解放される)」と見抜き、ビルドを止めます。参照が、それが指すデータより長生きすることは絶対に許さない——これが Rust の鉄則です。

正しい解決は、参照ではなく所有権ごと返すことです。

fn no_dangle() -> String {
    let s = String::from("hello");
    s                              // 参照ではなく s 自体(所有権)を返す → ムーブ
}   // 所有権が呼び出し側へ移るので、ここで解放されない。安全

s を(参照ではなく)そのまま返せば、所有権が呼び出し側へムーブし、データは生き続けます。解放されるのは、新しい所有者がスコープを抜けるときです。

では、「参照が指すデータより長生きしないこと」を、コンパイラはどうやって追跡しているのでしょう? その仕組みの名前が ライフタイム(lifetime) です。次章で正体を見ます。

試すには

dangle のようなコードは Rust では書けません(コンパイルエラー)。エラーメッセージに lifetime(ライフタイム)という言葉が出てくることに注目してください。次章で、この「参照の有効期間を追跡する仕組み」を学びます。ここまでで所有権の全体像はほぼ揃いました。