コンテンツにスキップ

Rustのエラー E0373: closure may outlive the current function(クロージャと借用)

E0373 は、クロージャがローカル変数を借用しているのに、その変数を所有している関数が終わったあとも、クロージャが動き続ける(あるいは存在し続ける)かもしれない、という意味です。初めて thread::spawn を呼んだときや、関数からクロージャを返したときによく出会います。うれしいことに、直し方はコンパイラがすでに教えてくれていて、ほとんどの場合 move というたった1語で済みます。このページでは、まず直し方を見せて、そのあとで理由を説明します。このシリーズのほかのエラーは Rustのコンパイルエラー解説 にまとめています。

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)。

ラベルを上から順に読んでいきましょう。

  1. ^^ may outlive borrowed value はクロージャ || を指しています。main が先に進む前にクロージャが終わっていると、Rust は約束できません。
  2. `scores` is borrowed here は、クロージャが scores を使っている場所を指しています。println! は値を読むだけなので、クロージャはベクタそのものではなく、scores への参照をキャプチャしました。
  3. function requires argument type to outlive 'static は、なぜそれが問題なのかを説明しています。thread::spawn は、プログラムが必要とする限りずっと生きていられるクロージャしか受け付けません。'static とは「なくなるかもしれないものを何も借用していない」という意味です。

友だちにノートを貸して「終わったら返してね」と言いながら、自分は別の町に引っ越そうとしている場面を想像してください。友だちの作業が遅ければ、ノートには返す先がなくなってしまいます。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));
}

実行結果:

6
11
▶ 実際に試してみよう

move を付けても、クロージャがどのクロージャのトレイトになるかは変わりません。データを読むだけの 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]
▶ 実際に試してみよう

「作業を分けて、あとで結果を使う」という場面では、スコープ付きスレッドがいちばんすっきりした答えです。共有状態のレッスンも、ここから始まります。

ループの中でスレッドを起動する。 クロージャが読むだけなら、ただの数値でも借用されてしまいます。

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 のページも参考にしてください。

理解度チェック

1. スレッドのクロージャが move なしで name を表示しています。E0373 が出るのはなぜでしょう?

2. move を付けましたが、main はスレッドを起動したあとでもベクタを表示したいです。いちばん簡単な直し方はどれでしょう?

3. move なしでスレッドがローカル変数を借用できるのは、どの道具を使ったときでしょう?

スコア: 0 / 3