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.
Le message d’erreur
Section intitulée « Le message d’erreur »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 scores7 | | }); | |______^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).
Ce que ça veut dire
Section intitulée « Ce que ça veut dire »Lisez les étiquettes de haut en bas :
^^ may outlive borrowed valuepointe vers la closure||. Rust ne peut pas promettre que la closure aura fini avant quemainne continue.`scores` is borrowed herepointe vers l’endroit où la closure utilisescores. Commeprintln!a seulement besoin de le lire, la closure a capturé une référence versscores, pas le vecteur lui-même.function requires argument type to outlive 'staticexplique pourquoi c’est un problème :thread::spawnn’accepte que des closures capables de vivre aussi longtemps que le programme en a besoin.'staticveut 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.
Pourquoi Rust fait ça
Section intitulée « Pourquoi Rust fait ça »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.
Comment corriger
Section intitulée « Comment corriger »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 252La 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 :
611move 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ésmain a toujours : ["Ana", "Ben"]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.00Total : 9.00Total : 5.00Prix : [4.5, 2.0, 3.5]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 = 316Toujours à nous : [90, 85, 77, 64]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.
Où vous le verrez souvent
Section intitulée « Où vous le verrez souvent »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.
Erreurs liées
Section intitulée « Erreurs liées »- E0382 : borrow of moved value : apparaît juste après avoir ajouté
move, quand vous réutilisez la variable en dehors de la closure. Clonez-la d’abord (solution 2) ou utilisezArc(solution 3). - E0597 : borrowed value does not live long enough : un proche cousin. Une référence est encore utilisée après la libération de la valeur vers laquelle elle pointe.
- E0515 : cannot return reference to local variable : le même problème « tout disparaît quand la fonction se termine », avec une simple référence au lieu d’une closure.
- E0106 : missing lifetime specifier : apparaît quand vous essayez de renvoyer une référence (ou un type de closure) qui emprunte quelque chose.
- Toutes les erreurs du compilateur : la liste complète des pages d’erreurs.
Petit quiz
Section intitulée « Petit quiz »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
Pour aller plus loin
Section intitulée « Pour aller plus loin »- E0373 — Rust error codes index (en anglais) : l’explication officielle de cette erreur, avec la correction
move. - Using Threads to Run Code Simultaneously — The Rust Book (en anglais) : la section « Using move Closures with Threads » décortique exactement cette erreur.
- Closures — The Rust Book (en anglais) : comment les closures capturent les variables, par référence, par référence mutable ou par valeur.
- Le mot-clé
move— documentation de la bibliothèque standard (en anglais) : ce quemovechange dans les captures d’une closure, avec de courts exemples. std::thread::scope— documentation de la bibliothèque standard (en anglais) : les threads à portée limitée, qui peuvent emprunter des données locales en toute sécurité.