RustのNLL(Non-Lexical Lifetimes)について

2023/02/26に公開

初めに

RustのNLL(Non-Lexical Lifetimes)について、調べてみたらドキュメントがなさそうだったのでまとめてみた。

NLL(Non-Lexical Lifetimes)って何?

NLL(Non-Lexical Lifetimes)は、Rust1.63以降に導入された機能。Rust2018と2015(migration mode)で使用できる。[1]

この機能は、元々はlifetimeを元にborrow checkしていたものを、control-flow graphで置き換えた物である。ライフタイムでなく制御フローを元に参照がどのくらい使用されるかを判断して最適化してくれるので、元々のborrow checkの問題点(後述)を解決したものになる。[2]

NLL導入以前の問題点

NLL以前は、lifetimeを元にborrow checkしていたため、下記のコードでコンパイルエラーとなっていた。
これは、sliceのmut参照がmainのスコープ内で生きているにも関わらず、pushがさらにmutな参照を取得しているために発生する。
(あるデータに対するmutな参照は同一スコープ内で一つしか取得できないことに注意。)

fn main() {
    let mut s = String::from("abc");
    let s_ref_mut = &mut s;  // ここでmutな参照を渡す
    add_string(s_ref_mut, 'd');
    s.push('e');  // 以前はここでエラーが起きていた。
}

fn add_string(s: &mut String, c: char) {
    s.push(c);
}

このコンパイルエラーを解決するには、追加でスコープを導入する必要があった。

fn main() {
    let mut s = String::from("abc");
    // コンパイルエラー回避のためにスコープを導入。
    {
        let s_ref_mut = &mut s;
        add_string(s_ref_mut, 'd');
    }
    s.push('e');
}

fn add_string(s: &mut String, c: char) {
    s.push(c);
}

これに対して、NLLはlifetimeではなく制御フローを元にデータの参照を使用する期間を判定して最適化するため、上記のコードでもコンパイルが通るようになる。

ただし、mutな参照を使い終わる前に別の参照を取得しようとすると、mutな参照が残っている間に新しい参照を取得しようとするため、コンパイルエラーになってしまうことには注意が必要になる。
mutな参照が入れ子にならないように気をつければ、この問題を回避することができる。

  • コンパイルエラーになるケース
fn main() {
    let mut s = String::from("abc");
    let s_ref_mut = &mut s;
    s.push('e');  // second mutable borrow occurs here
    add_string(s_ref_mut, 'd');
    s.push('e');
}

fn add_string(s: &mut String, c: char) {
    s.push(c);
}

  • コンパイルが通るケース
fn main() {
    let mut s = String::from("abc");
    let s_ref_mut = &mut s;
    add_string(s_ref_mut, 'd');
    
}

fn add_string(s: &mut String, c: char) {
    s.push(c);
}

終わりに

Rustは難しいと言われがちですが、こういう暗黙的な挙動を知ってるかどうかでハマり具合が変わってくるので、知っておくとよいと思います。
あと、質問やコメントいただけると嬉しいのでよろしくお願いします。

参考

脚注
  1. https://blog.rust-lang.org/2022/08/05/nll-by-default.html ↩︎

  2. 詳しい仕組みはRFCを参照のこと。自分は全く理解できてない…。 ↩︎

Discussion