Closed19

メモ: 匿名ライフタイム

ピン留めされたアイテム
Takanori IshikawaTakanori Ishikawa

匿名ライフタイムを理解するにあたって重要なのは、Edition 2018 で導入された

このふたつと、ライフタイムの省略ルールが重要。

さらに、表現としては、「'_ プレースホルダによって、ライフタイムの省略ルールに従った匿名ライフタイムが割り当てられる」が正確かも。これには以下の効果がある。

  • ライフタイムが省略されていることを明示する
  • 明示的にライフタイムを宣言しなくてもライフタイムを指定できる
  • Trait object に通常のライフタイム省略ルールを適用する
  • impl Trait に通常のライフタイム省略ルールを適用する?
    • まだ良くわからない
Takanori IshikawaTakanori Ishikawa

そもそもの話として、参照を持つ struct や enum のライフタイムが省略されていると、何かを借用していることが不明瞭という問題があった。

struct Foo<'a> {
    x: &'a u32
}

fn foo(x: &u32) -> Foo { // Foo が x を借用することが分からない
}

そのため、Edition 2018 からはライフタイムパラメータの省略が非推奨となり、Lint でも elided-lifetimes-in-paths ルールで対応されている(ただし、デフォルトでは allow)

なお、上記の Lint を含む Edition 2018 のイディオムは rust-2018-idioms グループに含まれている。

Takanori IshikawaTakanori Ishikawa
#![deny(elided_lifetimes_in_paths)]
struct Foo<'a> {
    x: &'a u32
}

fn foo(x: &u32) -> Foo {
    Foo { x }
}

これをコンパイルすると、

error: hidden lifetime parameters in types are deprecated
 --> src/main.rs:6:20
  |
6 | fn foo(x: &u32) -> Foo {
  |                    ^^^- help: indicate the anonymous lifetime: `<'_>`
  |
note: the lint level is defined here
 --> src/main.rs:1:9
  |
1 | #![deny(elided_lifetimes_in_paths)]
  |         ^^^^^^^^^^^^^^^^^^^^^^^^^
Takanori IshikawaTakanori Ishikawa

では、上記のエラーを直すことを考える。Edition 2018 以前では、明示的にライフタイムを指定するしか無かった。

fn foo<'a>(x: &'a u32) -> Foo<'a> {
    Foo { x }
}
Takanori IshikawaTakanori Ishikawa

ここでライフタイムの省略ルールを適用すると、以下のルールに従ってライフタイムを省略できるのであった。

  • 引数にはそれぞれ新しいライフタイムが割り当てられる。
  • 引数に たったひとつのライフタイムしかない場合 は、そのライフタイムがすべての返り値にも割り当てられる。

なので、以下のプログラムは明示的にライフタイムを指定したバージョンと同じになる。

fn foo(x: &u32) -> Foo {
    Foo { x }
}
Takanori IshikawaTakanori Ishikawa

しかし、非参照型へのライフタイムパラメータの省略は非推奨にしたいので、ライフタイムが省略されていることを '_ プレースホルダで明示することができるようになった。

fn foo(x: &u32) -> Foo<'_> {
    Foo { x }
}
Takanori IshikawaTakanori Ishikawa

さらに '_ プレースホルダはライフタイムパラメータを指定する箇所であれば関数以外でも使える。

  • 入力コンテキストでは、それぞれの '_ に新しいライフタイムが割り当てられる。
  • 出力コンテキストでは、それぞれの '_ に、すべての出力に割り当てられたライフタイムひとつが割り当てられる。

なので、impl でも使うことができる。今までは

impl<'a> Foo<'a> {...}

のように、まず、'a を宣言してから Foo に 'a を与えていたのを、

impl Foo<'_> {...}

のように書くことができる。ただし、注意してほしいのは、ここで与えたライフタイムを impl 内の関数で参照することはできない、ということ。

Takanori IshikawaTakanori Ishikawa

たとえば、構造体 Foo の impl を匿名ライフタイムを使って以下のように定義する。

struct Foo<'a> {
    a: &'a i32,
}

impl Foo<'_> {
    fn a(&self) -> &'_ i32 {
        self.a
    }
}

これは一見、正しく動くように見える。

fn main() {
    let a = 35;
    let x: &i32;
    
    let foo = Foo { a: &a };
    x = foo.a();

    println!("x = {}", x); // => 35
}

しかし、新しいスコープを導入して、以下のように書くとうまくいかない。

fn main() {
    let a = 35;
    let x: &i32;
    
    {
        let foo = Foo { a: &a };
        x = foo.a();
    }

    println!("x = {}", x);
}

結果

   Compiling playground v0.0.1 (/playground)
error[E0597]: `foo` does not live long enough
  --> src/main.rs:17:13
   |
17 |         x = foo.a();
   |             ^^^ borrowed value does not live long enough
18 |     }
   |     - `foo` dropped here while still borrowed
19 | 
20 |     println!("x = {}", x);
   |                        - borrow later used here

しかし、変数 aprintln!(...) の時点まで生きているはずだ。

struct Foo<'a> {
    a: &'a i32,
}

impl<'a> Foo<'a> {
    fn a(&self) -> &'a i32 {
        self.a
    }
}

fn main() {
    let a = 35;
    let x: &i32;
    
    {
        let foo = Foo { a: &a };
        x = foo.a();
    }

    println!("x = {}", x);  // 35
}
Takanori IshikawaTakanori Ishikawa

そもそも Trait object と impl Trait は異なるので、さらに調査必要


Trait object における '_

メソッドから impl Trait を返すときも '_ プレースホルダが必要になるが、これには Default trait object lifetimes が関係していそうだ。

struct Foo {
    items: Vec<i32>,
}

impl Foo {
    fn items(&self) -> impl Iterator<Item = i32> + '_ {
        self.items.iter().copied()
    }
}

fn main() {
    let foo = Foo {
        items: vec![1, 2, 3],
    };

    for x in foo.items() {
        println!("x = {}", x);
    }
}

items() メソッドの返り値から '_ プレースホルダーを取り除いてみる。

fn items(&self) -> impl Iterator<Item = i32> {
    self.items.iter().copied()
}

これをコンパイルすると以下のエラーになる。

Compiling playground v0.0.1 (/playground)
error[E0759]: `self` has an anonymous lifetime `'_` but it needs to satisfy a `'static` lifetime requirement
 --> src/main.rs:7:20
  |
6 |     fn items(&self) -> impl Iterator<Item = i32> {
  |              ----- this data with an anonymous lifetime `'_`...
7 |         self.items.iter().copied()
  |         ---------- ^^^^
  |         |
  |         ...is captured here...
  |
note: ...and is required to live as long as `'static` here
 --> src/main.rs:6:24
  |
6 |     fn items(&self) -> impl Iterator<Item = i32> {
  |                        ^^^^^^^^^^^^^^^^^^^^^^^^^
help: to declare that the `impl Trait` captures data from argument `self`, you can add an explicit `'_` lifetime bound
  |
6 |     fn items(&self) -> impl Iterator<Item = i32> + '_ {
  |                                                  ^^^^

error: aborting due to previous error

self の省略された匿名ライフタイムが 'static と互換でなければいけない」と怒られてしまった。しかし、ライフタイムの省略ルールによれば、

  • メソッドのレシーバが &Self または &mut Self の場合、返り値の省略されたライフタイムにも同じライフタイムが割り当てられる

のではないだろうか?

ここで Default trait object lifetimes が関係してくる。実は、Trait object については、ライフタイムの省略ルールが異なっているのだ。詳細はリンク先を読んでもらうとして、Trait object にライフタイムが一切含まれない場合は 'static が割り当てられる。ただし、'_ プレースホルダが使われた場合は、通常の省略ルールに従う。

item() メソッドの返り値のライフタイムは、明らかに 'static ではないので、ここでは '_ が必要になる、というわけだ。

Takanori IshikawaTakanori Ishikawa

Rust のライフタイムのテストケース
https://github.com/rust-lang/rust/blob/1.52.1/src/test/ui/impl-trait/lifetimes.rs/#L6-L28

use std::fmt::Debug;

fn any_lifetime<'a>() -> &'a u32 { &5 }

fn static_lifetime() -> &'static u32 { &5 }

fn any_lifetime_as_static_impl_trait() -> impl Debug {
    any_lifetime()
}

fn lifetimes_as_static_impl_trait() -> impl Debug {
    static_lifetime()
}

fn no_params_or_lifetimes_is_static() -> impl Debug + 'static {
    lifetimes_as_static_impl_trait()
}

fn static_input_type_is_static<T: Debug + 'static>(x: T) -> impl Debug + 'static { x }

fn type_outlives_reference_lifetime<'a, T: Debug>(x: &'a T) -> impl Debug + 'a { x }
fn type_outlives_reference_lifetime_elided<T: Debug>(x: &T) -> impl Debug + '_ { x }
Takanori IshikawaTakanori Ishikawa

Type parameter から引き継ぐ。なければ 'static で良さそう

以下の構造体がある時

struct Foo<'a> {
    items: Vec<&'a i32>
}

これは ok。たぶん、ライフタイムは '_

impl<'a> Foo<'a> {
    fn items(&self) -> impl IntoIterator<Item=&i32> {
        self.items.iter().copied()
    }
}

だめ。これもダメ。たぶん、ライフタイムは 'static

struct Foo<'a> {
    items: Vec<&'a i32>,
}

impl<'a> Foo<'a> {
    fn items(&self) -> impl IntoIterator<Item = i32> {
        self.items.iter().copied().map(|x| *x)
    }
}

これもダメ。たぶん、ライフタイムは 'a

impl<'a> Foo<'a> {
    fn items(&self) -> impl IntoIterator<Item=&'a i32> {
        self.items.iter().copied()
    }
}
Takanori IshikawaTakanori Ishikawa

clippy が警告を表示していくれる

error: explicit lifetimes given in parameter types where they could be elided (or replaced with `'_` if needed by type declaration)
 --> src/language_server/description.rs:9:1
  |
9 | fn format_type_specifier<'a>(ty: Option<TypeKind<'a>>) -> String {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `-D clippy::needless-lifetimes` implied by `-D warnings`
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes

error: aborting due to previous error
Takanori IshikawaTakanori Ishikawa

'static ライフタイムについて

  • static 宣言で定数を作成する
  • 文字列リテラル &'static str 型を持つ変数を作成する
    • これらは実行バイナリの一部として格納される。
    • プログラムが動作している間、常に有効

これらは 'static ライフタイム

pub fn downcast_ref<T: Error + 'static>(&self) -> Option<&T> { ... }

これが 'static ライフタイム境界

T 内の全ての参照は 'static ライフタイムよりも長く(つまり同じだけ)生きていなければならない。

Takanori IshikawaTakanori Ishikawa

dyn Trait と impl Trait

  • 引数の位置の impl Trait はトレイト境界のショートハンド
  • Trait object: dyn Trait
    • Java のオブジェクトに近い。メソッドは動的ディスパッチされる
    • ポインタで渡さなければならない
    • Generic な関数ではなく、トレイトを実装したあらゆる値を受け取れる
  • 返り値の位置の impl Trait はジェネリクスのショートハンドではない
    • ジェネリクスは、呼ぶ側が実際の型を決める
    • impl Trait は呼ばれる関数側が決める。関数の本体から推論される
    • 呼ぶ側はトレイト境界しか分からない
    • ただし、今のところ実際の型は一つのみ
Takanori IshikawaTakanori Ishikawa

この 3 種類のうちいずれを使うかの判断には、これらがどのように実装しているかを知っていると役に立つ

  • ジェネリクスと引数の impl Trait
    • monomorphisation (単相型、単相化?)
    • それぞれの型インスタンスに対して関数のコピーを作成する。
    • たとえば、fn f(b: impl Bar)FooBar で呼び出しているなら、関数のコピーがふたつできる。
    • 一切の間接呼び出しが発生しないのでパフォーマンスは良いが、コードサイズは増える
  • 返り値の impl Trait には monomorphisation は必要なく、単に具象型で置き換えられる。
  • Trait object
    • こちらも monomorphisation は必要ない
    • Trait object の型は fat pointer で実装されている
    • 参照.&dyn Bar は値への参照だけでなく vtable への参照を含む
    • つまり動的ディスパッチされる
Takanori IshikawaTakanori Ishikawa

Implicit bounds

  • SendSync のようなトレイトは、型を構成するそれぞれの型がこれらを実装している限り、自動で実装される
  • impl Trait が返り値で使われた場合、これらのトレイトは関数本体から暗黙のうちに推論される
  • つまり、+ Send + Syncimpl Trait に対して書く必要はない
    • Trait object では必要
Takanori IshikawaTakanori Ishikawa

匿名ライフタイムの記事ができてきたので、記事のメモをここに残しておく

このうち dyn トレイトが一番古くからある機能ですが、引数の位置の impl トレイトがもっとも簡単なので、先に説明します。

引数の位置の impl トレイト

ジェネリクスの型パラメーターにはトレイト境界を指定することができます。

fn println_value<T>(value: T)
where
    T: Display,
{
    println!("value = {}", value);
}

こう書くこともできます。

fn println_value<T: Display>(value: T) {
    println!("value = {}", value);
}

さらに、引数位置の impl トレイトを使うともっと簡単に書くことができます。

fn println_value(value: impl Display) {
    println!("value = {}", value);
}

型パラメータをなくすことができ、より直感的になりましたね。このように、引数位置の impl トレイトは、型パラメータとトレイト境界のより短い記法です。[^7]


  • 引数の位置の impl Trait はジェネリクスのトレイト境界のショートハンド
  • Trait object: dyn Trait
    • Java のオブジェクトに近い。メソッドは動的ディスパッチされる
    • ポインタで渡さなければならない
    • Generic な関数ではなく、トレイトを実装したあらゆる値を受け取れる
  • 返り値の位置の impl Trait はジェネリクスのショートハンドではない
    • ジェネリクスは、呼ぶ側が実際の型を決める
    • impl Trait は呼ばれる関数側が決める。関数の本体から推論される
    • 呼ぶ側はトレイト境界しか分からない
    • ただし、今のところ実際の型は一つのみ

この 3 種類のうちいずれを使うかの判断には、これらがどのように実装しているかを知っていると役に立つ

  • ジェネリクスと引数の impl Trait
    • monomorphisation (単相型、単相化?)
    • それぞれの型インスタンスに対して関数のコピーを作成する。
    • たとえば、fn f(b: impl Bar)FooBar で呼び出しているなら、関数のコピーがふたつできる。
    • 一切の間接呼び出しが発生しないのでパフォーマンスは良いが、コードサイズは増える
  • 返り値の impl Trait には monomorphisation は必要なく、単に具象型で置き換えられる。
  • Trait object
    • こちらも monomorphisation は必要ない
    • Trait object の型は fat pointer で実装されている
    • 参照.&dyn Bar は値への参照だけでなく vtable への参照を含む
    • つまり動的ディスパッチされる

Object Safety

  • すべてのトレイトが Trait object になれるわけではない
    • "Object safe" なトレイトのみ
  • なぜ必要かというと、&dyn Trait&impl Trait な引数に渡すため

Implicit bounds

  • SendSync のようなトレイトは、型を構成するそれぞれの型がこれらを実装している限り、自動で実装される
  • impl Trait が返り値で使われた場合、これらのトレイトは関数本体から暗黙のうちに推論される
  • つまり、+ Send + Syncimpl Trait に対して書く必要はない
    • Trait object では必要
  • 一方、ライフタイム境界はもっと複雑
  • 型変数と引数位置の impl Trait は暗黙的なライフタイム境界を持たない。
    • impl Trait の実体の型はスコープ中の型変数に依存する
    • ライフタイム境界も同様
  • 何も見つからなければ 'sttaic

’static` ライフタイムについて

  • static 宣言で定数を作成する
  • 文字列リテラル &'static str 型を持つ変数を作成する
    • これらは実行バイナリの一部として格納される。
    • プログラムが動作している間、常に有効

これらは 'static ライフタイム

pub fn downcast_ref<T: Error + 'static>(&self) -> Option<&T> { ... }

これが 'static ライフタイム境界

T 内の全ての参照は 'static ライフタイムよりも長く(つまり同じだけ)生きていなければならない。あるいは参照を含まない(こちらのケースが多い)


clippy が警告を表示していくれる

error: explicit lifetimes given in parameter types where they could be elided (or replaced with `'_` if needed by type declaration)
 --> src/language_server/description.rs:9:1
  |
9 | fn format_type_specifier<'a>(ty: Option<TypeKind<'a>>) -> String {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `-D clippy::needless-lifetimes` implied by `-D warnings`
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes

error: aborting due to previous error

記事にするときの構成

  • はじめに
    • プライベートで Rust を書き始めて数ヶ月
      • ライフタイムに苦しめられている
    • 匿名ライフタイムについてよく分からなかったので調べた
      • そもそも何なのか?
      • 何のためにあるのか?
      • 日々のプログラミングでどのように役立つのか?
  • 匿名ライフタイムとは何か?
    • 匿名ライフタイムの例
      • 適当なプログラム
      • 標準ライブラリの例
      • impl Trait を返すときの例
    • Edition 2018 で導入された詳細
      • これらをちゃんと読めば詳細は理解できる
      • 何のために導入されて、どう役立つのかを中心に書きます
  • 匿名ライフタイムの意義と効用
    • 関数での匿名ライフタイム
      • ライフタイムの省略ルールをおさらい
      • 省略されていることを明示するための匿名ライフタイム
      • 何が嬉しいのか?
        • 参照を持つ構造体を返す例
        • Edition 2018 での新しい警告
    • impl ブロックでの匿名ライフタイム
      • Edition 2018 で導入された
      • Trait を実装するときに便利
    • Trait object の匿名ライフタイム
      • impl Trait を返すときの例再掲
      • ライフタイムの省略ルール再び
      • 匿名ライフタイムでルールを変える
  • まとめ
    • 構造体が他の参照を借用していることを示すのはいいこと
      • Edition 2018 の idiom 警告は有効にしておいた方がいい
    • 匿名ライフタイムを闇雲に使わない
      • 借用チェッカーとライフタイムが通っても意味的に正しいとは限らない
      • 匿名ライフタイムで十分だと明らかな場合のみ使う
        • 関数の引数と返り値が省略されたライフタイムで動くが、ライフタイムを明示したい場合
        • Trait を実装するが、メソッドでライフタイムが不要な場
このスクラップは2021/07/07にクローズされました