Rustのエラー E0373: closure may outlive the current function(クロージャと借用)
E0373 は、クロージャがローカル変数を借用しているのに、その変数を所有している関数が終わったあとも、クロージャが動き続ける(あるいは存在し続ける)かもしれない、という意味です。初めて thread::spawn を呼んだときや、関数からクロージャを返したときによく出会います。うれしいことに、直し方はコンパイラがすでに教えてくれていて、ほとんどの場合 move というたった1語で済みます。このページでは、まず直し方を見せて、そのあとで理由を説明します。このシリーズのほかのエラーは Rustのコンパイルエラー解説 にまとめています。
エラーメッセージ
Section titled “エラーメッセージ”error[E0373]: closure may outlive the current function, but it borrows `scores`, which is owned by the current function --> src/main.rs:5:32 |5 | let handle = thread::spawn(|| { | ^^ may outlive borrowed value `scores`6 | println!("Scores: {scores:?}"); // ❌ scores を借用しているだけ | ------ `scores` is borrowed here |note: function requires argument type to outlive `'static` --> src/main.rs:5:18 |5 | let handle = thread::spawn(|| { | __________________^6 | | println!("Scores: {scores:?}"); // ❌ scores を借用しているだけ7 | | }); | |______^help: to force the closure to take ownership of `scores` (and any other referenced variables), use the `move` keyword |5 | let handle = thread::spawn(move || { | ++++「クロージャは現在の関数より長く生きるかもしれませんが、現在の関数が所有している scores を借用しています」という意味です。最後の help: は、move キーワードを使えばクロージャに scores の所有権を持たせられる、と教えてくれています。
このエラーが出るプログラムはこちらです。
use std::thread;
fn main() { let scores = vec![90, 85, 77]; let handle = thread::spawn(|| { println!("Scores: {scores:?}"); // ❌ scores を借用しているだけ }); handle.join().unwrap();}async のコードでは、同じエラーコードで少し違う言い回しになります。“async block may outlive the current function, but it borrows user_id”(async ブロックが現在の関数より長く生きるかもしれないのに、user_id を借用している)です。中身は同じエラーで、直し方も同じです(async move)。
どういう意味?
Section titled “どういう意味?”ラベルを上から順に読んでいきましょう。
^^ may outlive borrowed valueはクロージャ||を指しています。mainが先に進む前にクロージャが終わっていると、Rust は約束できません。`scores` is borrowed hereは、クロージャがscoresを使っている場所を指しています。println!は値を読むだけなので、クロージャはベクタそのものではなく、scoresへの参照をキャプチャしました。function requires argument type to outlive 'staticは、なぜそれが問題なのかを説明しています。thread::spawnは、プログラムが必要とする限りずっと生きていられるクロージャしか受け付けません。'staticとは「なくなるかもしれないものを何も借用していない」という意味です。
友だちにノートを貸して「終わったら返してね」と言いながら、自分は別の町に引っ越そうとしている場面を想像してください。友だちの作業が遅ければ、ノートには返す先がなくなってしまいます。Rust はこの約束を断り、代わりにノートをあげてしまうように求めているのです。
Rust がこうする理由
Section titled “Rust がこうする理由”クロージャは、使える中でいちばん安上がりな方法で変数をキャプチャします。変数を読むだけなら、& で借用します。.map() や .filter() に渡すような、その場ですぐ実行されるクロージャなら、これはとても便利です。詳しくはクロージャのページを見てください。
新しいスレッドは事情が違います。スレッドはどれだけ長く動き続けるかわかりませんし、join を呼ばない限り main はそれを待ちません。もしスレッドが scores への参照を持っていて、main が先に終わったら、scores は破棄され、スレッドは解放済みのメモリを読むことになります。これが「ダングリング参照」で、C や C++ のスレッドを使うコードで特によくあるバグの1つです。Rust はこれをコンパイル時に止めてくれます。
クロージャを返すときも同じことが起きます。関数のローカル変数(と引数)は、関数が戻るときに破棄されます。それをまだ借用しているクロージャは、何もない場所を指すことになってしまいます。
直し方1:move を付ける(いつもの答え)
Section titled “直し方1:move を付ける(いつもの答え)”ほとんどの場合はこれを選びましょう。rustc が提案しているのも、まさにこの方法です。move を付けると、クロージャは使う変数の所有権を取り、変数はクロージャと一緒に移動します。
use std::thread;
fn main() { let scores = vec![90, 85, 77]; let handle = thread::spawn(move || { let total: i32 = scores.iter().sum(); println!("点数: {scores:?}、合計 {total}"); }); handle.join().unwrap();}実行結果:
点数: [90, 85, 77]、合計 252クロージャを返す関数でも、同じ直し方が使えます。move がないと、|x| x + n は引数 n を借用しますが、n は make_adder が戻った時点でなくなってしまいます。move を付ければ、それぞれのクロージャが自分専用の n のコピーを持ちます。
fn make_adder(n: i32) -> impl Fn(i32) -> i32 { move |x| x + n}
fn main() { let add_five = make_adder(5); let add_ten = make_adder(10); println!("{}", add_five(1)); println!("{}", add_ten(1));}実行結果:
611move を付けても、クロージャがどのクロージャのトレイトになるかは変わりません。データを読むだけの move クロージャは Fn のままで、何度でも呼び出せます。
直し方2:値をあとでも使うなら、先に clone する
Section titled “直し方2:値をあとでも使うなら、先に clone する”move したあと、その変数はクロージャのものになります。main があとでそれを使うと E0382(borrow of moved value)が出て、rustc はムーブする前に値を clone するよう提案してきます。クロージャ用のコピーを作り、元の値は手元に残しましょう。
use std::thread;
fn main() { let guests = vec![String::from("Ana"), String::from("Ben")]; let guests_for_thread = guests.clone();
let handle = thread::spawn(move || { println!("スレッドから見えるゲスト: {}人", guests_for_thread.len()); }); handle.join().unwrap();
println!("main にもまだある: {guests:?}");}実行結果:
スレッドから見えるゲスト: 2人main にもまだある: ["Ana", "Ben"]小さなリストなら、clone するコストは安く、まったく問題ありません。データが大きいときや、たくさんのスレッドが必要とするときは、Arc を使って(直し方3)1つのコピーをみんなで共有しましょう。
直し方3:複数のスレッドで Arc を使って共有する
Section titled “直し方3:複数のスレッドで Arc を使って共有する”複数のスレッドが同じデータを読む必要があるときはこれを選びましょう。Arc(“atomically reference counted”、アトミックな参照カウント)を使うと、たくさんの所有者が1つの値を共有できます。Arc::clone はポインタをコピーしてカウンタを1増やすだけで、データそのものはコピーしません。
use std::sync::Arc;use std::thread;
fn main() { let prices = Arc::new(vec![4.5, 2.0, 3.5]); let mut handles = Vec::new();
for discount in [0.0, 0.1, 0.5] { let prices = Arc::clone(&prices); // スレッドごとに1つずつハンドルを用意 handles.push(thread::spawn(move || { let total: f64 = prices.iter().sum(); total * (1.0 - discount) })); }
for handle in handles { println!("合計: {:.2}", handle.join().unwrap()); } println!("価格: {prices:?}");}実行結果:
合計: 10.00合計: 9.00合計: 5.00価格: [4.5, 2.0, 3.5]Arc だけでは、読み取り専用の共有になります。スレッドがデータを書き換える必要もあるなら、Arc<Mutex<T>> のように包みましょう。並行処理と共有状態のページで説明しています。
直し方4:std::thread::scope で安全に借用する
Section titled “直し方4:std::thread::scope で安全に借用する”スレッドが短い作業の間だけデータを必要としていて、ムーブや clone ではなく借用で済ませたいときはこれを選びましょう。thread::scope の中で起動したスレッドは、scope が戻る前に必ず終わることが保証されています。そのため、ローカル変数を借用しても安全で、move も必要ありません。
use std::thread;
fn main() { let scores = vec![90, 85, 77, 64]; let (first, second) = scores.split_at(2);
let (a, b) = thread::scope(|s| { let h1 = s.spawn(|| first.iter().sum::<i32>()); let h2 = s.spawn(|| second.iter().sum::<i32>()); (h1.join().unwrap(), h2.join().unwrap()) }); // ここで両方のスレッドが終わっている
println!("{a} + {b} = {}", a + b); println!("まだ手元にある: {scores:?}");}実行結果:
175 + 141 = 316まだ手元にある: [90, 85, 77, 64]「作業を分けて、あとで結果を使う」という場面では、スコープ付きスレッドがいちばんすっきりした答えです。共有状態のレッスンも、ここから始まります。
よく見かける場面
Section titled “よく見かける場面”ループの中でスレッドを起動する。 クロージャが読むだけなら、ただの数値でも借用されてしまいます。
for worker in 1..=3 { handles.push(thread::spawn(|| { println!("Worker {worker} started"); // ❌ `worker` を借用している }));}直し方:thread::spawn(move || ...) にします。worker は i32 なので、move はそれぞれのスレッドに値をコピーするだけです。
Box に入れたクロージャを返す。 Box<dyn Fn() -> String> を返す関数の中で Box::new(|| greeting.clone()) と書くと、ローカル変数 greeting を借用してしまい、rustc は note: closure is returned here(クロージャはここで返されている)と付け加えます。直し方:Box::new(move || greeting.clone()) にします。詳しくは Fn・FnMut・FnOnce のページで。
async のタスク(Tokio など)。 tokio::spawn は thread::spawn と同じように動きます。タスクは現在の関数が戻ったずっとあとまで動くかもしれないので、'static なデータが必要です。tokio::spawn(async { ... }) と書いて中でローカル変数を使うと、“async block may outlive the current function” が出ます。直し方:tokio::spawn(async move { ... }) にし、タスクの外でもまだ必要なものは、先に clone か Arc::clone しておきます。async/await のページも参考にしてください。
関連するエラー
Section titled “関連するエラー”- E0382: borrow of moved value:
moveを付けたあと、クロージャの外でその変数をまた使うとすぐに出ます。先に clone する(直し方2)か、Arcを使いましょう(直し方3)。 - E0597: borrowed value does not live long enough:近い親戚のようなエラーです。指している値が破棄されたあとも、参照がまだ使われています。
- E0515: cannot return reference to local variable:「関数が戻るとなくなる」という同じ問題を、クロージャではなく普通の参照で起こしたものです。
- E0106: missing lifetime specifier:何かを借用している参照(やクロージャの型)を返そうとしたときに出ます。
- コンパイルエラー解説の一覧:エラーのページをすべて並べています。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. スレッドのクロージャが move なしで name を表示しています。E0373 が出るのはなぜでしょう?
2. move を付けましたが、main はスレッドを起動したあとでもベクタを表示したいです。いちばん簡単な直し方はどれでしょう?
3. move なしでスレッドがローカル変数を借用できるのは、どの道具を使ったときでしょう?
スコア: 0 / 3
さらに詳しく
Section titled “さらに詳しく”- E0373 — Rust error codes index(英語):
moveによる直し方も載っている、このエラーの公式の説明です。 - スレッドを使用してコードを同時に走らせる — The Rust Programming Language 日本語版:「スレッドでmoveクロージャを使用する」の節で、まさにこのエラーを順を追って説明しています。
- クロージャ:環境をキャプチャする匿名関数 — The Rust Programming Language 日本語版:クロージャが変数を参照・可変参照・値のどれでキャプチャするかがわかります。
movekeyword — standard library docs(英語):moveがクロージャのキャプチャをどう変えるかを、短い例で説明しています。std::thread::scope— standard library docs(英語):ローカルなデータを安全に借用できる、スコープ付きスレッドの説明です。