Chapter 2: 複雑なライフタイム問題

学習目標

  • 複数のライフタイムパラメータの扱いを習得する
  • ライフタイム境界の高度な使い方を学ぶ
  • 関数ポインタとクロージャのライフタイムを理解する
  • GAT (Generic Associated Types) を活用する
  • 実践的な難問とその解決策を学ぶ
  • ---

    2.1 複数のライフタイムパラメータ

    2.1.1 独立したライフタイム

    struct Context<'s, 'c> {
        //           ^^  ^^
        //           |   コンテキストのライフタイム
        //           文字列のライフタイム
        string_data: &'s str,
        config: &'c Config,
    }
    
    impl<'s, 'c> Context<'s, 'c> {
        fn new(s: &'s str, c: &'c Config) -> Self {
            Context {
                string_data: s,
                config: c,
            }
        }
    
        // 's と 'c は独立
        fn get_string(&self) -> &'s str {
            self.string_data
        }
    
        fn get_config(&self) -> &'c Config {
            self.config
        }
    }
    

    2.1.2 ライフタイムの順序制約

    struct Parser<'a, 'b> where 'b: 'a {
        //                      ^^^^^^^
        //                      'b は 'a より長生き
        input: &'a str,
        cache: &'b Cache,
    }
    
    // なぜこの制約が必要か
    impl<'a, 'b> Parser<'a, 'b>
    where
        'b: 'a,
    {
        fn parse(&self) -> Result<&'a str, Error> {
            // cache('b)を使って input('a)を処理
            // cacheはinputより長生きする必要がある
            Ok(self.input)
        }
    }
    

    ---

    2.2 高階関数とライフタイム

    2.2.1 関数を返す関数

    // 間違った例
    fn returns_closure() -> Box<dyn Fn(&str) -> &str> {
        //                              ^^^^     ^^^^
        //                              ライフタイムが不明!
        Box::new(|x| x)
    }
    
    // 正しい例: HRTB使用
    fn returns_closure() -> Box<dyn for<'a> Fn(&'a str) -> &'a str> {
        Box::new(|x| x)
    }
    

    2.2.2 クロージャとキャプチャ

    fn example() {
        let string = String::from("hello");
    
        // クロージャが string を借用
        let closure = || {
            println!("{}", string);
        };
    
        // string のライフタイムはクロージャの存在期間を含む必要
        closure();
    }
    
    // より複雑な例
    fn complex_closure<'a>(s: &'a str) -> impl Fn() -> &'a str {
        move || s
    }
    

    ---

    2.3 ライフタイム省略の落とし穴

    2.3.1 予期しない省略

    // これは...
    fn foo(x: &str, y: &str) -> &str {
        if x.len() > y.len() { x } else { y }
    }
    
    // こう解釈される(エラー!)
    fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &str {
        //                                     ^^^^
        //                                     どのライフタイム?
    }
    
    // 正しく書く
    fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() > y.len() { x } else { y }
    }
    

    2.3.2 メソッドでの省略

    struct Parser<'a> {
        input: &'a str,
    }
    
    impl<'a> Parser<'a> {
        // 省略前
        fn parse<'b>(&'b self) -> &'a str {
            self.input
        }
    
        // 省略後(推論される)
        fn parse(&self) -> &'a str {
            //    ^^^^^^
            //    &'b self と推論されるが、
            //    戻り値は 'a を使う
            self.input
        }
    }
    

    ---

    2.4 Generic Associated Types (GAT)

    2.4.1 LendingIterator

    // 標準の Iterator
    trait Iterator {
        type Item;
        fn next(&mut self) -> Option<Self::Item>;
    }
    
    // GAT を使った LendingIterator
    trait LendingIterator {
        type Item<'a> where Self: 'a;
        //       ^^^^
        //       ライフタイム付き関連型
    
        fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
    }
    
    // 実装例
    impl<'s, T> LendingIterator for WindowsMut<'s, T> {
        type Item<'a> = &'a mut [T] where Self: 'a;
    
        fn next<'a>(&'a mut self) -> Option<Self::Item<'a>> {
            // スライスを borrowing して返せる
            None  // 実装省略
        }
    }
    

    2.4.2 GATの応用

    trait Database {
        type Transaction<'a> where Self: 'a;
    
        fn begin<'a>(&'a mut self) -> Self::Transaction<'a>;
    }
    
    struct MyDb;
    
    impl Database for MyDb {
        type Transaction<'a> = Transaction<'a>;
    
        fn begin<'a>(&'a mut self) -> Transaction<'a> {
            Transaction { db: self }
        }
    }
    
    struct Transaction<'a> {
        db: &'a mut MyDb,
    }
    

    ---

    2.5 自己参照とPin

    2.5.1 問題の本質

    struct SelfRef {
        data: String,
        ptr: *const String,  // data を指す
    }
    
    fn broken() {
        let mut s = SelfRef {
            data: String::from("hello"),
            ptr: std::ptr::null(),
        };
        s.ptr = &s.data as *const _;
    
        // 問題: s をムーブすると ptr が無効に!
        let s2 = s;  // s.ptr は古いアドレスを指す
    }
    

    2.5.2 Pin による解決

    use std::pin::Pin;
    use std::marker::PhantomPinned;
    
    struct SelfRef {
        data: String,
        ptr: *const String,
        _pin: PhantomPinned,
    }
    
    impl SelfRef {
        fn new(data: String) -> Pin<Box<Self>> {
            let mut boxed = Box::pin(SelfRef {
                data,
                ptr: std::ptr::null(),
                _pin: PhantomPinned,
            });
    
            let self_ptr: *const String = &boxed.data;
            unsafe {
                let mut_ref = Pin::as_mut(&mut boxed);
                Pin::get_unchecked_mut(mut_ref).ptr = self_ptr;
            }
    
            boxed
        }
    
        fn get_data(self: Pin<&Self>) -> &str {
            &self.data
        }
    
        fn get_ptr_data(self: Pin<&Self>) -> &str {
            unsafe { &*self.ptr }
        }
    }
    

    ---

    2.6 実践的な難問

    2.6.1 キャッシュとライフタイム

    問題

    struct Cache {
        data: HashMap<String, String>,
    }
    
    impl Cache {
        // エラー:どのライフタイム?
        fn get_or_insert(&mut self, key: &str, default: &str) -> &str {
            if let Some(value) = self.data.get(key) {
                value  // ライフタイム 'a
            } else {
                self.data.insert(key.to_string(), default.to_string());
                self.data.get(key).unwrap()  // ライフタイム 'b
            }
        }
    }
    

    解決策1:ライフタイムを明示

    impl Cache {
        fn get_or_insert<'a>(&'a mut self, key: &str, default: &str) -> &'a str {
            if !self.data.contains_key(key) {
                self.data.insert(key.to_string(), default.to_string());
            }
            self.data.get(key).unwrap()
        }
    }
    

    解決策2:エントリーAPI

    impl Cache {
        fn get_or_insert<'a>(
            &'a mut self,
            key: &str,
            default: &str,
        ) -> &'a str {
            self.data
                .entry(key.to_string())
                .or_insert_with(|| default.to_string())
        }
    }
    

    2.6.2 イテレータの連鎖

    問題

    struct Parser<'a> {
        input: &'a str,
    }
    
    impl<'a> Parser<'a> {
        fn parse(&mut self) -> impl Iterator<Item = &str> {
            //                                        ^^^^
            //                                        ライフタイム不明
            self.input.split_whitespace()
        }
    }
    

    解決策

    impl<'a> Parser<'a> {
        fn parse<'b>(&'b mut self) -> impl Iterator<Item = &'a str> + 'b {
            //       ^^                                     ^^        ^^
            //       メソッドのライフタイム
            //                                              inputのライフタイム
            //                                                         イテレータの境界
            self.input.split_whitespace()
        }
    }
    

    ---

    2.7 ライフタイムサブタイピングの高度な使用

    2.7.1 共変性の活用

    fn extend_lifetime<'a, 'b>(r: &'a &'b i32) -> &'a &'a i32
    where
        'b: 'a,
    {
        // &'b i32 は &'a i32 のサブタイプ('b: 'a のとき)
        unsafe { std::mem::transmute(r) }
        // 実際には transmute は不要(共変性で自動変換)
    }
    

    2.7.2 反変性の理解

    fn shorten_fn_lifetime<'a, 'b>(
        f: fn(&'b i32),
    ) -> fn(&'a i32)
    where
        'a: 'b,
    {
        // fn(&'b i32) は fn(&'a i32) のサブタイプ('a: 'b のとき)
        f
    }
    

    ---

    2.8 デバッグテクニック

    2.8.1 ライフタイムの明示

    // 型エラーが出たら、全てのライフタイムを明示して理解する
    fn debug_lifetimes<'a, 'b, 'c>(
        x: &'a str,
        y: &'b str,
        z: &'c str,
    ) -> &'a str {
        // どのライフタイムが問題か明確に
        x
    }
    

    2.8.2 rustc の詳細エラー

    $ rustc --explain E0623
    
    # または
    $ cargo clippy -- -W clippy::needless_lifetimes
    

    ---

    2.9 まとめ

    2.9.1 複雑なライフタイム問題の解決戦略

    1. ライフタイムを全て明示して理解
    2. 各参照がどこから来てどこに行くかトレース
    3. 省略規則を理解して不要な注釈を削除
    4. コンパイラのエラーメッセージを熟読
    5. Pin/GAT などの高度な機能を検討
    

    2.9.2 よくあるパターン

  • 複数のライフタイムは独立に扱う
  • where句でライフタイム順序を明示
  • GAT で柔軟な関連型を実現
  • Pin で自己参照を安全に扱う

---

練習問題

問題1

複数のライフタイムを持つ構造体を設計し、それぞれの役割を説明しなさい。

問題2

LendingIterator を実装し、標準の Iterator との違いを説明しなさい。

問題3

なぜ &'a mut T が invariant なのか、variance の観点から説明しなさい。

---

次の章へ

Chapter 3: 自己参照構造体へ →