Aller au contenu

Erreur Rust E0373 : closure may outlive the current function (move manquant)

E0373 veut dire qu’une closure (fonction anonyme) emprunte une variable locale, alors qu’elle pourrait continuer à s’exécuter (ou continuer à exister) après la fin de la fonction à laquelle appartient cette variable. On la rencontre en général la première fois qu’on appelle thread::spawn ou qu’on renvoie une closure depuis une fonction. Bonne nouvelle : le compilateur vous donne déjà la correction, et c’est presque toujours un seul mot, move. Cette page montre d’abord la correction, puis explique pourquoi. Pour les autres erreurs de cette série, voir Les erreurs du compilateur 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:?}"); // ❌ ne fait qu'emprunter 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:?}"); // ❌ ne fait qu'emprunter 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 || {
| ++++

En français : « la closure peut survivre à la fonction en cours, mais elle emprunte scores, qui appartient à la fonction en cours ». L’aide propose d’ajouter le mot-clé move pour que la closure prenne possession de scores.

Voici le programme qui la provoque :

use std::thread;
fn main() {
let scores = vec![90, 85, 77];
let handle = thread::spawn(|| {
println!("Scores: {scores:?}"); // ❌ ne fait qu'emprunter scores
});
handle.join().unwrap();
}

Avec du code async, vous verrez la même erreur avec des mots un peu différents : « async block may outlive the current function, but it borrows user_id ». C’est la même erreur, avec la même correction (async move).

Lisez les étiquettes de haut en bas :

  1. ^^ may outlive borrowed value pointe vers la closure ||. Rust ne peut pas promettre que la closure aura fini avant que main ne continue.
  2. `scores` is borrowed here pointe vers l’endroit où la closure utilise scores. Comme println! a seulement besoin de le lire, la closure a capturé une référence vers scores, pas le vecteur lui-même.
  3. function requires argument type to outlive 'static explique pourquoi c’est un problème : thread::spawn n’accepte que des closures capables de vivre aussi longtemps que le programme en a besoin. 'static veut dire « n’emprunte rien qui pourrait disparaître ».

Imaginez que vous prêtiez votre carnet à un ami en lui disant « rends-le-moi quand tu as fini », alors que vous êtes sur le point de déménager dans une autre ville. Si votre ami travaille lentement, le carnet n’aura plus d’adresse où revenir. Rust refuse ce marché et vous demande plutôt de donner le carnet.

Une closure capture les variables de la manière la moins coûteuse qui fonctionne. Si elle ne fait que lire une variable, elle l’emprunte avec &. C’est parfait pour les closures qui s’exécutent tout de suite, comme celles que vous passez à .map() ou .filter(). Vous en saurez plus sur la page Les closures.

Un nouveau thread, c’est différent. Il peut tourner pendant un temps quelconque, et main ne l’attend pas, sauf si vous appelez join. Si le thread gardait une référence vers scores et que main se terminait en premier, scores serait libéré et le thread lirait de la mémoire libérée. C’est une « référence pendante » (dangling reference), l’un des bugs les plus courants dans le code multithread en C et en C++. Rust l’arrête dès la compilation.

La même chose se produit quand vous renvoyez une closure. Les variables locales de la fonction (et ses paramètres) sont libérées quand elle se termine. Une closure qui les emprunterait encore pointerait vers du vide.

Solution 1 : ajouter move (la réponse habituelle)

Section intitulée « Solution 1 : ajouter move (la réponse habituelle) »

Choisissez cette solution presque à chaque fois. C’est exactement ce que suggère rustc. move dit à la closure de prendre l’ownership (possession) des variables qu’elle utilise, pour qu’elles voyagent avec elle :

use std::thread;
fn main() {
let scores = vec![90, 85, 77];
let handle = thread::spawn(move || {
let total: i32 = scores.iter().sum();
println!("Scores : {scores:?}, total {total}");
});
handle.join().unwrap();
}

Sortie :

Scores : [90, 85, 77], total 252
▶ Essayez par vous-même

La même correction fonctionne pour une fonction qui renvoie une closure. Sans move, |x| x + n emprunte le paramètre n, qui disparaît dès que make_adder se termine. Avec move, chaque closure garde sa propre copie de 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));
}

Sortie :

6
11
▶ Essayez par vous-même

move ne change pas le trait de closure que vous obtenez. Une closure move qui ne fait que lire ses données reste Fn et peut être appelée autant de fois que vous voulez.

Solution 2 : cloner d’abord, quand vous avez encore besoin de la valeur

Section intitulée « Solution 2 : cloner d’abord, quand vous avez encore besoin de la valeur »

Après move, la variable appartient à la closure. Si main l’utilise ensuite, vous obtiendrez E0382 (« borrow of moved value »), et rustc vous suggère de cloner la valeur avant de la déplacer. Faites une copie pour la closure et gardez l’original :

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!("Le thread voit {} invités", guests_for_thread.len());
});
handle.join().unwrap();
println!("main a toujours : {guests:?}");
}

Sortie :

Le thread voit 2 invités
main a toujours : ["Ana", "Ben"]
▶ Essayez par vous-même

Cloner une petite liste ne coûte presque rien, c’est tout à fait correct. Si les données sont grosses, ou si beaucoup de threads en ont besoin, utilisez Arc (solution 3) pour qu’ils partagent tous une seule copie.

Solution 3 : partager avec Arc entre plusieurs threads

Section intitulée « Solution 3 : partager avec Arc entre plusieurs threads »

Choisissez cette solution quand plusieurs threads doivent lire les mêmes données. Arc (atomically reference counted, compteur de références atomique) permet à plusieurs propriétaires de partager une seule valeur. Arc::clone ne copie qu’un pointeur et incrémente un compteur ; les données elles-mêmes ne sont pas copiées :

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); // une poignée par thread
handles.push(thread::spawn(move || {
let total: f64 = prices.iter().sum();
total * (1.0 - discount)
}));
}
for handle in handles {
println!("Total : {:.2}", handle.join().unwrap());
}
println!("Prix : {prices:?}");
}

Sortie :

Total : 10.00
Total : 9.00
Total : 5.00
Prix : [4.5, 2.0, 3.5]
▶ Essayez par vous-même

Arc seul permet uniquement un partage en lecture. Si les threads doivent aussi modifier les données, enveloppez-les dans un Arc<Mutex<T>>. Voir La concurrence et L’état partagé.

Solution 4 : emprunter en toute sécurité avec std::thread::scope

Section intitulée « Solution 4 : emprunter en toute sécurité avec std::thread::scope »

Choisissez cette solution quand les threads n’ont besoin des données que pour une tâche courte et que vous préférez emprunter plutôt que déplacer ou cloner. Les threads lancés dans thread::scope ont la garantie de se terminer avant que scope ne rende la main : emprunter des variables locales est donc sûr, et move n’est pas nécessaire :

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())
}); // ici, les deux threads ont terminé
println!("{a} + {b} = {}", a + b);
println!("Toujours à nous : {scores:?}");
}

Sortie :

175 + 141 = 316
Toujours à nous : [90, 85, 77, 64]
▶ Essayez par vous-même

Les threads à portée limitée (scoped threads) sont la réponse la plus propre pour « répartir le travail, puis utiliser les résultats ». La leçon L’état partagé commence par eux.

Lancer des threads dans une boucle. Même un simple nombre est emprunté si la closure ne fait que le lire :

for worker in 1..=3 {
handles.push(thread::spawn(|| {
println!("Worker {worker} started"); // ❌ emprunte `worker`
}));
}

Correction : thread::spawn(move || ...). worker est un i32, donc move le copie simplement dans chaque thread.

Renvoyer une closure dans une Box. Box::new(|| greeting.clone()) dans une fonction qui renvoie Box<dyn Fn() -> String> emprunte la variable locale greeting, et rustc ajoute note: closure is returned here. Correction : Box::new(move || greeting.clone()). Plus de détails dans Fn, FnMut et FnOnce.

Les tâches async (Tokio et compagnie). tokio::spawn fonctionne comme thread::spawn : la tâche peut s’exécuter bien après la fin de la fonction en cours, elle a donc besoin de données 'static. Écrire tokio::spawn(async { ... }) en utilisant une variable locale à l’intérieur donne « async block may outlive the current function ». Correction : tokio::spawn(async move { ... }), et clonez (ou Arc::clone) tout ce dont vous avez encore besoin en dehors de la tâche. Voir Async/await.

Petit quiz

1. La closure d'un thread affiche name sans utiliser move. Pourquoi obtenez-vous E0373 ?

2. Vous ajoutez move, mais main doit encore afficher le vecteur après avoir lancé le thread. Quelle est la correction la plus simple ?

3. Quel outil permet à des threads d'emprunter des variables locales sans move ?

Score : 0 / 3