対象読者
以下のコードがコンパイルエラーになる理由がわからない方。
struct Hoge<'a> {
i: &'a usize
}
fn func<'a>(_v: &'a mut Hoge<'a>) {
// noop
}
fn main() {
let i = 1;
let mut hoge = Hoge{i: &i};
func(&mut hoge); // OK
func(&mut hoge); // ERROR
}
はじめに
この記事ではRustのライフタイムとVarianceについて例を示し、最後に上記のコードがなぜコンパイルが通らないかについて考える。
サブタイプとVariance
まずそもそも「Varianceとはなんぞや」という方は上記記事をおすすめする。( 日本語版は未訳のようだ)
サブタイプとVarianceについてわかりやすく書かれているが、ライフタイムについてはそれほど詳しく書かれているわけではない。
以下は上記記事にあるVariance早見表である。
| 'a | T | U | |
|---|---|---|---|
| &'a T | covariant | covariant | |
| &'a mut T | covariant | invariant | |
| Box | covariant | ||
| Vec | covariant | ||
| UnsafeCell | invariant | ||
| Cell | invariant | ||
| fn(T) -> U | contravariant | covariant | |
| *const T | covariant | ||
| *mut T | invariant |
うっかりすると「ライフタイムにはcovariantしかないのだな」と勘違いしてしまうがそうではない。
注意すべきはTやUがライフタイムを含んでいる場合T,Uと同じVarianceとなる 1 ということである。
例を示すと関数の引数に&'a mut Hoge<'b>このような指定が合った場合'bは&mut TのTと同様invariantになるということである。
ライフタイムとVariance
型に関するサブタイピングのVarianceを考える機会というのはそう多くはない(個人の感想です)が、 Rustではライフタイムにもサブタイプが存在するため、Varianceについて頻繁に接することとなる。例を見ていこう。
ライフタイムとサブタイプ
例を見ていく前にライフタイムとサブタイプについて少し。
Rustのライフタイムにおいてライフタイム'bがライフタイム'aの全期間を含んでいる場合に'bは'aのサブタイプとなる('b <: 'a)。
Rustにおいて'staticは全期間にわたるライフタイムとなるため、'staticはすべてのライフタイムのサブタイプとみなされる。(任意のライフタイム'aに対して'static <: 'a)
以降の例ではコードを簡潔にするためライフタイムのサブタイプとして'staticを多用していくがあくまでサブタイプの例にすぎない。
共変(covariant)
共変であるばあいサブタイプが許容される。
例としては、以下のコードは問題なくコンパイルが通る。
fn func<'a>(p: &'a i32, q: &'a i32) {}
fn main() {
let i = 0;
let j: &'static i32 = &1;
func(&i, j);
}
jのライフタイムは'staticである。func()は二つの引数で同じライフタイム'aを要求しているので、func(&i, j)の呼び出し時'aは雑に考えて&iのライフタイム、もしくは'staticとして推論されるはずである。
-
'aを'staticであると仮定すると、&iのライフタイムは'staticのサブタイプではないためコンパイルできない -
'aを&iのライフタイムであると仮定すると、'static'はiのライフタイムのサブタイプ('static<:&iのライフタイム)であるためコンパイルできる
よって'aはiのライフタイムとして推論される。
反変(contravariant)
次の例はコンパイルエラーとなる。
fn func<'a>(a: &'a i32, f: fn(&'a i32)) {
}
fn main() {
let i = 0;
func(&i, |_: &'static i32| {}); // ERROR
}
表にあるとおりf(T)のTは反変の位置にあり、その影響はライフタイムにも及ぶ。そのため上記コードではfunc(&i, |_: &'static i32| {})に対して
-
'aを&iのライフタイムと推論すると反変位置に'staticがあるため許容されない -
'aを'staticと推論すると共変位置の&iのライフタイムがサブタイプではないため許容されない
となってコンパイルエラーとなる。
コンパイルできる例
fn func<'a, F: FnMut(&'a i32)>(a: &'a i32, f: F) {
}
fn main() {
let i = 1;
let ref_i = &i;
{
let mut v = Vec::new();
let j = 2;
v.push(&j);
func(ref_i, |x: &i32| {
v.push(&x);
});
} // P1
println!("{}", *ref_i);
}
このコードはコンパイルが通る。funcの2番目の引数のライフタイムはvの中身のライフタイムと同じと推論されるはずなのでP1までである。
ref_iのライフタイムは明らかにそれより長く反変の関係が成立している。
では次の場合はどうだろうか。
以下のコードはコンパイルが通るが、実際の動きを考えると不思議だ。
func()が何もしないため、func()から抜けたときに&iのライフタイムは終わってしまう。そうするとP1までのライフタイム'aに対して共変関係を満たせずコンパイルエラーとなってしまうのではないか。
fn func<'a, F: FnMut(&'a i32)>(a: &'a i32, f: F) {
}
fn main() {
let i = 1;
{
let mut v = Vec::new();
let j = 2;
v.push(&j);
func(&i, |x: &i32| {
v.push(&x);
});
println!("{:?}", v);
} // P1
}
だがこのコードはコンパイルエラーにはならない。
ライフタイムは推論時、他と競合しない限り必要な分だけ伸張されるからだ。今の場合P1までの'aと共変関係を満たすように&iのライフタイムが伸張されることになり、結局&iのライフタイムもP1まで伸びることとなる。
これは面白い結果である。func()の実装が
fn func<'a, F: FnMut(&'a i32)>(a: &'a i32, mut f: F) {
f(a);
}
こうだった場合、&iはvに格納されるためライフタイムはP1まで続くことが必須となるがRustのコンパイラはvariantのルールからそのことを推論しているわけである。
不変(invariant)
&mut TのTは不変になるが、&mut Vec<&'a i32>のような場合'aが不変となる。
fn func<'a>(x: &mut Vec<&'a i32>, y: &mut Vec<&'a i32>) {
}
この関数に対して次のコードはコンパイルが通らない。
fn main() {
let mut v1 = Vec::new();
let i = 1;
v1.push(&i);
{
let mut v2 = Vec::new();
let j = 1;
v2.push(&j);
func(&mut v1, &mut v2);
}
println!("{:?}", v1);
}
func()の引数では'aが両方不変位置のため、'aはv1,V2のどちらかのライフタイムと推論されるしか無いが、
-
'aをv1のライフタイムと推論すると、y: &mut Vec<'a i32>のライフタイムがv2のライフタイムと整合しない -
'aをv2のライフタイムと推論すると、x: &mut Vec<'a i32>のライフタイムがv1のライフタイムと整合しない
となるためである。
ちなみにprintln!()行がない場合はコンパイルが通ってしまうが、これはV1,v2のライフタイムがfunc()の呼び出しで同時に終わるためである。
冒頭の例について考える
最後に冒頭の例について考えてみる。
struct Hoge<'a> {
i: &'a usize
}
fn func<'a>(_v: &'a mut Hoge<'a>) {
// noop
}
fn main() {
let i = 1;
let mut hoge = Hoge{i: &i}; // P1
func(&mut hoge); // OK P2
func(&mut hoge); // ERROR P3
}
一見P2で作られたhogeの可変参照はその場でライフタイムが終わるため、P3の呼び出しも問題ないように思えるが、コンパイルしてみるとP2の可変参照がP3でも生きているかのようなエラーとなる。
今まで見てきた、ライフタイムとVarianceの考え方からこれは以下のように説明できる。
-
func()引数の2番めの'aは不変位置にあるため、'aは&iのライフタイムと同一と推論される - 引数1番目の
'aは共変位置にあるため、hoge参照のライフタイムは&iのライフタイムのサブタイプである必要がある - そのため、P2で作られた
hogeの参照のライフタイムはP3に及ぶこととなり、コンパイルエラーとなる
余談
余程のことがない限り&'a mut Hoge<'a>のように、参照のライフタイムを型のライフタイムパラメータに使いまわす(?)ような書き方はしないと思う。アンチパターンと言ってもよいのではないだろうか。
おわりに
Rustにおいてはライフタイムがサブタイプの関係を持つためVarianceを考える機会に頻繁に遭遇するが、そこを解説した文書をあまり見かけず苦労したため自分の手で記述してみた。Rustコード読み書きの一助となれば幸いである。
-
厳密には必ずしも同じとはならないのだが本記事では同じとなる例しかでてこない ↩