LoginSignup
15
10

More than 1 year has passed since last update.

【Rust】ライフタイムとVariance

Last updated at Posted at 2021-06-04

対象読者

以下のコードがコンパイルエラーになる理由がわからない方。

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 TTと同様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のライフタイム)であるためコンパイルできる

よって'aiのライフタイムとして推論される。

反変(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);
}

こうだった場合、&ivに格納されるためライフタイムはP1まで続くことが必須となるがRustのコンパイラはvariantのルールからそのことを推論しているわけである。

不変(invariant)

&mut TTは不変になるが、&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が両方不変位置のため、'av1,V2のどちらかのライフタイムと推論されるしか無いが、

  • 'av1のライフタイムと推論すると、y: &mut Vec<'a i32>のライフタイムがv2のライフタイムと整合しない
  • 'av2のライフタイムと推論すると、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の考え方からこれは以下のように説明できる。

  1. func()引数の2番めの'aは不変位置にあるため、'a&iのライフタイムと同一と推論される
  2. 引数1番目の'aは共変位置にあるため、hoge参照のライフタイムは&iのライフタイムのサブタイプである必要がある
  3. そのため、P2で作られたhogeの参照のライフタイムはP3に及ぶこととなり、コンパイルエラーとなる

余談

余程のことがない限り&'a mut Hoge<'a>のように、参照のライフタイムを型のライフタイムパラメータに使いまわす(?)ような書き方はしないと思う。アンチパターンと言ってもよいのではないだろうか。

おわりに

Rustにおいてはライフタイムがサブタイプの関係を持つためVarianceを考える機会に頻繁に遭遇するが、そこを解説した文書をあまり見かけず苦労したため自分の手で記述してみた。Rustコード読み書きの一助となれば幸いである。

  1. 厳密には必ずしも同じとはならないのだが本記事では同じとなる例しかでてこない

15
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
10