ライフタイムは「生存期間」という意味だ。プログラムでは、ライフタイムには2種類ある。「値のライフタイム」と「参照のライフタイム」だ。値のライフタイムはその値のスコープが切れるまでで、参照のライフタイムは値への参照が使われる期間になる。
特に参照のライフタイムをきちんと意識することは重要だ。CやC++といった言語では、参照まわりの生存期間をしっかり守らないと脆弱性が発生する。
一方Rustでは、参照のライフタイムをコンパイル時に解析し、不適切な場合にはコンパイルエラーとして検出するようになっている。Rustのメモリー安全性を支える根本がこのライフタイムなのだ。
値のライフタイムと参照のライフタイムの関係
初歩的な例を見つつ、Rustのライフタイムについて確認していこう。
次に示すのは参照を使ったプログラムの例だ。まず初期化されていない変数 「r」を用意する。次に、ブロック内で「x」という変数に束縛された「5」という値への参照をrに束縛する。最後に、rが指す内容を標準出力しようとしている。
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("{}", r);
}
このプログラムをビルドしようとするとコンパイルエラーになる。「参照のライフタイムは値のライフタイムよりも長くなってはならない」というルールがあるからだ。
xはブロックの最後で寿命が終わる。これによりxの値である5もスコープが切れて解放される。ブロックの外でrが指す内容を表示しようとしても、rは生きているものの5は存在しないため、コンパイルエラーになる。
このプログラムのライフタイムを模式図で示すと、次のような包含関係になっている。参照のライフタイムが値のライフタイムよりも長いのでコンパイルエラーになるのだ。
この問題を解決する方法の1つは、値のライフタイムの終端地点と参照のライフタイムの終端地点が合うように修正することだ。先ほどのプログラムを次のように直すと、コンパイルエラーは検出されず、実行すると「5」が表示される。
fn main() {
let r;
{
let x = 5;
r = &x;
println!("{}", r);
}
}
xに束縛した5という値がブロックの終端で解放される点は同じだが、それよりも前にrが指す値を標準出力するよう修正している。これにより、参照のライフタイムが値のライフタイムを越えることがなくなった。
ライフタイムという概念を導入することで「ダングリングポインター」の問題を解決できる。ダングリングポインターは、解放されたメモリー領域を指し続けてしまうポインターを指す。CやC++では、ダングリングポインターを生み出すようなプログラムでもコンパイルが通ってしまい、実行時に未定義動作になってしまう。ダングリングポインターは一般に深刻な脆弱性になる。