Rustの生存期間パラメータとは何なのか

September 22, 2022

Rustの生存期間パラメータ(ライフタイム注釈)がわかりにくいので、に書いてあったことを要約してみることにしました。

まず生存期間とは何か。生存期間は変数にもあるし、参照(変数ではなく参照型の値。例えば、&xは参照)にもあるものである。

変数の生存期間と、参照の生存期間はコンパイラによって判断される。変数であれば、初期化された時点から、スコープから外れるまでが生存期間となる。一方、参照の生存期間は、プログラム実行中にその参照が安全に利用できる期間である。

参照の生存期間に対しては、2つの制約がある。

一つ目の制約は、「ある変数xに対して、変数xへの参照(&x)は、xよりも長生きしてはならない」、というもの。言い換えると「変数xの生存期間は&xの生存期間を包含しなければならない」、となると思う。この制約は複数の要素を所有する変数の各要素の参照についてもあてはまる。例えば、vという整数のベクタの生存期間は&v[1]の生存期間を包含しなければならない。

二つ目の制約は、「ある参照を変数rに格納する場合、その参照型の値(つまり参照)は、その変数の生存期間、すなわち初期化された時点からスコープから外れる時までの、全体で有効でなければならない」というもの。言い換えると「変数rの生存期間は、格納する参照の生存期間に包含されなければならない」、となると思う。この制約は複数の参照を所有する変数にもあてはまる。例えば、参照のベクタを作るような場合、すべての参照の生存期間が、ベクタを所有する変数の生存期間を包含しなければならない。

参照はこれら二つの制約を満たしていないと、ダングリングポインタとなってしまう期間をもつことになるので、そのようなコードをコンパイルしようとするとコンパイラがエラーを返す。

次に生存期間パラメータについて。生存期間パラメータは関数や構造体の定義で用いられるもので、参照が上記の制約を満たしているかどうかをコンパイラが判定する際に参照される。

関数の引数や戻り値が参照型の場合、それぞれの生存期間を表すパラメータ(生存期間パラメータ、あるいはライフタイム注釈とも呼ばれる)を定義できる。例えば、fn g<‘a>(p: &’a i32)のようなシグネチャの’aが生存期間パラメータで、このシグネチャの場合の’aは任意の生存期間(最も短い場合はgへの呼び出しを囲む期間)を意味する。関数の生存期間パラメータを省略した場合、コンパイラが必要な場所にそれぞれ独立した生存期間を割り当てるので、生存期間がどうあるべきかが自明であれば省略してもかまわない。例えばfn g(p: &i32)というシグネチャは、前述したfn g<`a>(p: &’a i32)というシグネチャとしてコンパイラに扱われる。

コンパイラは関数gをコンパイルする際、gのシグネチャからこの関数が呼び出し終了後にも生き残る場所にpを保存することはないと知り、渡された参照の生存期間として可能な限り短い期間(関数gの呼び出し)を想定するので、関数gの内部でその参照をstatic変数に格納しようとするとエラーになる。参照の生存期間がいつまでかもわからないのに、プログラム実行全体を生存期間とするstatic変数に格納するのは非常に危険(ダングリングポインタになるかもしれない)なので、エラーとすることで安全性を確保している。

引数と戻り値が両方とも参照であるような関数のシグネチャ、例えばfn smallest(v: &[i32]) -> &i32というシグネチャは、コンパイラによって生存期間パラメータが省略されている、とみなされる。明示的に生存期間パラメータを書くとfn smallest<‘a>(v: &’a [i32]) -> &’a i32となる。つまりコンパイラはこれらの参照が同じ生存期間を持つ、と仮定する。

コンパイラはこのシグネチャから、この関数を呼び出して戻り値を受け取った変数は関数に渡した引数の生存期間に包含されなければならない、と想定する。そのため以下のようなコードはエラーとなる。

fn smallest<'a>(v: &'a [i32]) -> &'a i32 {
  let mut s = &v[0];
  for r in &v[1..] {
    if *r < *s { s = r; }
  }
  s
}

let s;
{
  let parabola = [9, 4, 1, 0, 1, 4, 9];
  s = smallest(&parabola);
}
assert_eq!(*s, 0); // sはparabolaの参照の生存期間に包含されていないので、エラー。

このように、引数と戻り値の生存期間パラメータによって、関数に渡す参照と関数が返す参照の関係をコンパイラが判断し、安全に使われることを保証している。

参照を含む構造体の場合についても、関数と同様に生存期間パラメータを定義できるが、関数のように省略はできず、明示的に書く必要がある。

struct S<'a> {
  r: &'a i32
}

例えば上のようにS型を定義すると、式S { r: &x }は、ある生存期間’aを持つSを作る(言い換えるとSの生存期間が’a)。そして、rに保持される参照&xの生存期間は’aを包含していなければならず、’aはSを収めた何かの生存期間を包含していなければならない、とコンパイラは想定する。

struct S<'a> {
  r: &'a i32
}
let s;
{
  let x = 10;
  s = S { r: &x };
}
assert_eq!(*s.r, 10);

例えば上の例では’aはSを収めたsの生存期間を包含し、且つ、Sの中のrに保持される参照&xの生存期間は’aを包含していなければならないが、この2つの制約が同時に成立するような’aは存在しないためエラーとなる。

これまでの例では生存期間パラメータは1つしかなかったが、関数、構造体とも、2つ以上を持たせることも可能。例えば、fn f<‘a, ‘b>(r: &’a i32, s: &’b i32) -> &’a i32 { r } というシグネチャだと’a, ‘bはそれぞれ独立した生存期間として扱われるし、

struct S<'a, 'b> {
  x: &'a i32,
  y: &'b i32
}

のように構造体を定義すれば’a, ‘bはそれぞれ独立した生存期間となる。このように複数(独立)にすると生存期間に対する制約を緩めることになる。