Un atelier Rust · Édition d'ApprentissageA Rust Workshop · Learning Edition

Move semantics, ou la valeur qu'on déplace Move semantics, or the value you move

Le code naïf croit copier : let b = a. En Rust, la possession se transfère — a ne possède plus rien, et le réutiliser ne compile pas. Trois lignes, et tout le modèle de Rust tient dedans : un propriétaire, un drop, zéro double-free. Naive code thinks it copies: let b = a. In Rust, ownership transfers — a owns nothing anymore, and reusing it won't compile. Three lines, and all of Rust's model fits inside: one owner, one drop, zero double-free.

AudienceAudience
Dev venant d'un langage à GC, prêt à raisonner possession Dev from a GC language, ready to reason about ownership
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●○○○ PossessionMoveTradeoff
SommaireContents ·
01CadrageFraming3 min

Deux lettres, une seule donnée. Trace le propriétaire.Two names, one value. Trace the owner.

Trois lignes que tout le monde écrit le premier jour. Elles ne compilent pas — et le refus du compilateur, ici, n'est pas un caprice de syntaxe : c'est le modèle entier de Rust qui tient dans un seul let. La possession se transfère, elle ne se duplique pas.Three lines everyone writes on day one. They don't compile — and the compiler's refusal here is no syntactic quibble: it's all of Rust's model fitting inside a single let. Ownership transfers, it does not duplicate.

Le code qu'on croit anodinThe code we think is harmless
let a = String::from("hello");
let b = a;            // on croit copier a dans b
println!("{a}");      // ← et ici, le compilateur refuse
Qui possède le String ?Who owns the String?
a
déplacé · inaccessiblemoved · inaccessible
possession transféréeownership moved
b
possède "hello"owns "hello"

Le String n'a pas été copié. let b = a a transféré sa possession à b — et a, du coup, ne possède plus rien.The String wasn't copied. let b = a moved its ownership to b — and so a no longer owns anything.

Le réflexe du volumeThe volume's reflex

« Qui possède cette donnée — et qu'est-ce que je casse en la passant ailleurs ? » Chaque valeur a, à tout instant, exactement un propriétaire. L'affecter, la passer à une fonction, la retourner : autant de transferts de cette possession unique."Who owns this data — and what do I break by passing it elsewhere?" Every value has, at any instant, exactly one owner. Assigning it, passing it to a function, returning it: each is a transfer of that single ownership.

Pourquoi ce numéro ouvre la série.Why this issue opens the series.

Tout le reste de Rust — emprunts, lifetimes, Send/Sync, Arc — est une conséquence de cette règle. On ne commence pas par la syntaxe des structs : on commence par un String qu'on croit copier et qu'on a déplacé. Comprends le move, et le borrow checker cesse d'être un adversaire.Everything else in Rust — borrows, lifetimes, Send/Sync, Arc — is a consequence of this rule. We don't start with struct syntax: we start with a String you think you copied and actually moved. Understand the move, and the borrow checker stops being an opponent.

02La surpriseThe surprise4 min

En JS, b = a partage. En Rust, il déménage.In JS, b = a shares. In Rust, it moves out.

Quand on vient d'un langage à GC, « assigner » veut dire « faire pointer une deuxième variable sur la même donnée ». Deux noms, un objet, mutation partagée — et un bug d'aliasing qui compile sans broncher. Rust prend le contre-pied : assigner, c'est céder.Coming from a GC language, 'assigning' means 'make a second variable point at the same data'. Two names, one object, shared mutation — and an aliasing bug that compiles without a peep. Rust takes the opposite stance: to assign is to hand over.

JavaScript — le partage silencieuxJavaScript — the silent share
let a = { tag: "hello" };
let b = a;             // b et a pointent le MÊME objet
b.tag = "bye";
console.log(a.tag);    // "bye" — a a muté dans ton dos

Rien ne plante. a et b désignent le même objet : muter l'un mute l'autre. Le jour où ce code casse en production, le bug est à mille lignes de l'assignation. Le compilateur n'avait rien à dire — il n'y avait, pour lui, aucune erreur.Nothing crashes. a and b name the same object: mutating one mutates the other. The day this breaks in production, the bug is a thousand lines from the assignment. The compiler had nothing to say — to it, there was no error.

Le bug que JS te laisse écrire, Rust ne te laisse pas le compiler. C'est tout le marché.The bug JS lets you write, Rust won't let you compile. That's the entire bargain.
03Le transfertThe transfer5 min

Un propriétaire, un drop. Voilà le théorème.One owner, one drop. That's the theorem.

Passer une valeur à une fonction, c'est lui en céder la possession — exactement comme l'affectation. Pourquoi cette intransigeance ? Parce qu'une valeur a, à tout instant, un seul propriétaire ; et quand ce propriétaire sort de portée, sa donnée est libérée une fois, exactement. Pas de double libération possible : elle est inexprimable.Passing a value to a function hands over its ownership — exactly like assignment. Why the rigidity? Because a value has, at any instant, a single owner; and when that owner goes out of scope, its data is freed once, exactly. No double-free is possible: it is inexpressible.

Passer = céderPassing = handing over
fn consume(s: String) {        // consume PREND possession
    println!("{s}");
}                              // ici s est droppé : la mémoire est libérée

let name = String::from("Ada");
consume(name);                 // possession transférée à la fonction
println!("{name}");            // error[E0382] : name a été moved

Pour réutiliser name après l'appel, deux issues honnêtes : retourner la valeur (let name = consume(name);) pour récupérer la possession, ou — au prochain numéro — l'emprunter au lieu de la céder (consume(&name)). Cloner « pour que ça passe » est le réflexe à éviter ; on y vient au chapitre 4.To reuse name after the call, two honest exits: return the value (let name = consume(name);) to get ownership back, or — next issue — borrow it instead of giving it away (consume(&name)). Cloning 'to make it pass' is the reflex to avoid; we get to it in chapter 4.

L'exception qui confirme : les types CopyThe exception that confirms: Copy types

Copy — duplication impliciteCopy — implicit duplication
let x = 5;        // i32 : type Copy (vit sur la pile)
let y = x;        // x est COPIÉ, pas déplacé
println!("{x}");  // 5 — x est toujours là, intact

Les types de taille fixe qui vivent sur la pile (i32, bool, char, les tuples de Copy) implémentent Copy : les dupliquer est trivial, alors Rust le fait sans rien dire. Aucun tas à libérer, aucun risque de double drop.Fixed-size types living on the stack (i32, bool, char, tuples of Copy) implement Copy: duplicating them is trivial, so Rust does it silently. No heap to free, no risk of a double drop.

Clone — duplication expliciteClone — explicit duplication
let a = String::from("hello");
let b = a.clone();   // duplication explicite du tas
println!("{a}");     // hello — a possède toujours sa copie

Pour ce qui vit sur le tas (String, Vec), Rust refuse de copier dans ton dos : ça coûte une allocation. Tu dois écrire .clone(). La verbosité est le message : « cette copie coûte — la veux-tu vraiment ? »For what lives on the heap (String, Vec), Rust refuses to copy behind your back: it costs an allocation. You must write .clone(). The verbosity is the message: 'this copy costs — do you really want it?'

Le coûtThe cost

Ce que le move coûte : une friction à l'écriture. On ne peut plus balader une valeur partout par habitude ; il faut décider, à chaque passage, si on la cède, on la rend ou on l'emprunte. C'est précisément cette décision forcée qui rend le use-after-free et le double-free impossibles — pas un test à écrire, pas un linter à configurer : un théorème prouvé par le type-checker.What the move costs: writing friction. You can no longer pass a value around by habit; at each handoff you must decide whether you give it, return it, or borrow it. It is exactly this forced decision that makes use-after-free and double-free impossible — no test to write, no linter to configure: a theorem proved by the type-checker.

04Le piègeThe trap4 min

.clone() partout, ou comment racheter le GC qu'on a fui..clone() everywhere, or how to buy back the GC you fled.

Le réflexe du débutant venant d'un langage à GC : dès que le compilateur crie « moved value », coller un .clone() jusqu'à ce que ça passe. Ça marche. Et ça réintroduit, à la main et au prix fort, exactement les copies et allocations que Rust voulait te faire voir. Le compilateur ne criait pas pour t'embêter — il te demandait de décider.The beginner's reflex coming from a GC language: the moment the compiler shouts 'moved value', slap on a .clone() until it passes. It works. And it reintroduces, by hand and at full price, exactly the copies and allocations Rust wanted you to see. The compiler wasn't shouting to annoy you — it was asking you to decide.

Le contre-exemple — cloner pour faire taire le compilateurThe counter-example — cloning to silence the compiler
fn total(items: Vec<Item>) -> u64 { /* ... */ }

let cart = vec![/* 10 000 items */];
let t = total(cart.clone());   // clone pour « que ça passe »
render(cart);                  // on réutilise cart après

Ce .clone() duplique dix mille items sur le tas, à chaque appel, uniquement parce qu'on n'a pas voulu réfléchir à qui possède le panier. La valeur n'avait jamais besoin d'être copiée : total veut juste la lire.This .clone() duplicates ten thousand items on the heap, on every call, solely because we didn't want to think about who owns the cart. The value never needed copying: total just wants to read it.

Le bon réflexe — emprunter une vue, ne pas posséderThe right reflex — borrow a view, don't own
fn total(items: &[Item]) -> u64 { /* ... */ }   // emprunte, ne possède pas

let cart = vec![/* 10 000 items */];
let t = total(&cart);          // on prête cart, on ne le cède pas
render(&cart);                 // cart est toujours à nous, zéro copie

Une référence (&cart) : total regarde le panier sans en prendre possession, donc rien à céder, rien à cloner, rien à reconstruire. C'est le sujet du prochain numéro (Vol 1 · №02, l'emprunt) — mais le réflexe naît ici : avant de cloner, demande-toi si tu voulais seulement lire.A reference (&cart): total looks at the cart without taking ownership, so nothing to give up, nothing to clone, nothing to rebuild. That's the subject of the next issue (Vol 1 · №02, borrowing) — but the reflex is born here: before cloning, ask whether you only meant to read.

Quand clone est correctWhen clone is correct

Cloner n'est pas un péché — c'est un choix. Quand tu as vraiment besoin de deux propriétaires indépendants de la donnée (l'un mute, l'autre garde l'original ; une copie part dans un thread), .clone() est la réponse juste, explicite et lisible. Le problème n'est jamais le clone : c'est le clone réflexe, celui qu'on pose sans savoir pourquoi.Cloning is no sin — it's a choice. When you genuinely need two independent owners of the data (one mutates, the other keeps the original; a copy goes off into a thread), .clone() is the right answer, explicit and readable. The problem is never the clone: it's the reflex clone, the one dropped in without knowing why.

Le test mentalThe mental test

Devant un .clone() que tu t'apprêtes à écrire : « Ai-je besoin de deux copies vivant leur vie séparément — ou est-ce que je voulais juste prêter la donnée le temps d'un appel ? » Si c'est la seconde, ce n'est pas un clone qu'il te faut : c'est un &.Facing a .clone() you're about to write: 'Do I need two copies living separate lives — or did I just want to lend the data for the span of a call?' If it's the latter, it's not a clone you need: it's an &.

05Bilan & éditoWrap-up & editorial2 min

Trois issues pour une valeur qu'on donne.Three exits for a value you give away.

Le move n'est pas une restriction de plus à contourner : c'est la pièce qui fait tenir tout l'édifice. Une fois le réflexe en place, le reste de Rust se lit comme ses conséquences.The move isn't one more restriction to dodge: it's the keystone that holds the whole structure. Once the reflex is in place, the rest of Rust reads as its consequences.

Tu veux…You want to…Le gesteThe moveCe que ça coûteWhat it costs
CéderHand overAffecter ou passer la valeur : f(v). La possession part, l'ancien nom devient inutilisable.Assign or pass the value: f(v). Ownership leaves, the old name becomes unusable.Rien — c'est le défaut. Mais tu perds l'accès à l'ancien nom.Nothing — it's the default. But you lose access to the old name.
RendreReturnRetourner la valeur : let v = f(v);. La fonction emprunte la possession le temps de l'appel, puis la rend.Return the value: let v = f(v);. The function takes ownership for the call, then gives it back.Verbeux quand répété. Souvent le signe qu'un emprunt serait plus juste.Verbose when repeated. Often a sign a borrow would fit better.
PrêterLendEmprunter : f(&v). La fonction lit (ou mute) sans posséder. Vol 1 · №02.Borrow: f(&v). The function reads (or mutates) without owning. Vol 1 · №02.Une règle nouvelle à tenir (aliasing XOR mutation) — le sujet du prochain numéro.A new rule to uphold (aliasing XOR mutation) — the next issue's subject.
DupliquerDuplicateCloner : v.clone(). Deux propriétaires indépendants, pour de vrai.Clone: v.clone(). Two genuinely independent owners.Une allocation et une copie. Juste si tu en as besoin ; un piège si c'est par réflexe.An allocation and a copy. Right if you need it; a trap if it's reflexive.
« Value borrowed after move » n'est pas une punition. C'est le compilateur qui te dit : tu as déjà donné cette valeur — décide qui la possède."Value borrowed after move" is not a punishment. It's the compiler telling you: you already gave this value away — decide who owns it.
Mot de l'éditeurFrom the editor

Le compilateur ne te barre pas la route.The compiler isn't blocking your way.

On présente souvent le move comme la première épreuve d'un parcours du combattant. C'est l'inverse. Tant qu'on le vit comme une contrainte à neutraliser — à coups de clone, plus tard de Rc<RefCell>, parfois d'unsafe — on passe son temps à reconstruire à l'exécution les bugs que Rust avait éliminés à la compilation. Le jour où le move devient une question qu'on se pose soi-même — qui possède, et pour combien de temps ? — le borrow checker cesse d'être un adversaire.The move is often pitched as the first ordeal of an obstacle course. It's the opposite. As long as you live it as a constraint to neutralize — with clones, later with Rc<RefCell>, sometimes with unsafe — you spend your time rebuilding at runtime the very bugs Rust eliminated at compile time. The day the move becomes a question you ask yourself — who owns this, and for how long? — the borrow checker stops being an opponent.

Prochain numéro : l'emprunt. Comment regarder une donnée sans la posséder — et la seule règle qui transforme une course de données en erreur de compilation : aliasing XOR mutation.Next issue: borrowing. How to look at data without owning it — and the single rule that turns a data race into a compile error: aliasing XOR mutation.

Retour au kiosqueBack to newsstand