Vol. 9 · № 02 · Un atelier RustA Rust workshop

clone, Cow & l'allocation cachée clone, Cow & the hidden allocation

Le .clone() pour faire taire le compilateur cache une allocation — souvent un emprunt qu'on n'a pas su écrire. Cow n'alloue que si l'on mute vraiment. The .clone() to silence the compiler hides an allocation — often a borrow you couldn't express. Cow allocates only if you actually mutate.

AudienceAudience
Dev maîtrisant possession et emprunts (Vol 1), à l'aise avec String/&str Dev fluent in ownership and borrows (Vol 1), comfortable with String/&str
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●●○ CoûtCowclone
SommaireContents ·
№ 01CadrageFraming3 min

L'opposé de l'abstraction effacée : .clone() fait un vrai travail.The opposite of the erased abstraction: .clone() does real work.

Le numéro précédent montrait une abstraction que le compilateur efface : la chaîne d'itérateurs ne coûte rien à l'exécution. .clone() est son exact contraire — une opération qui fait un travail bien réel, et qui ne s'évapore pas. Pour un type qui possède de la mémoire sur le tas (String, Vec, HashMap), cloner veut dire allouer un nouveau tampon et y recopier tous les octets. Venant d'un langage à GC, on n'y prête aucune attention : là-bas, « passer » un objet ne le copie pas, on partage une référence et le ramasse-miettes gère le reste. En Rust, .clone() est explicite précisément parce qu'il a un coût qu'on doit voir. Le danger est de le dégainer par réflexe — chaque fois qu'un emprunt gêne, qu'un move dérange : un petit .clone() et ça compile. Chacun est une allocation silencieuse, et le plus souvent le signe d'un emprunt qu'on n'a pas su écrire.The previous issue showed an abstraction the compiler erases: the iterator chain costs nothing at runtime. .clone() is its exact opposite — an operation that does very real work, and doesn't evaporate. For a type owning heap memory (String, Vec, HashMap), cloning means allocating a new buffer and copying all the bytes into it. Coming from a GC language, you pay it no attention: there, 'passing' an object doesn't copy it, you share a reference and the garbage collector handles the rest. In Rust, .clone() is explicit precisely because it has a cost you must see. The danger is drawing it by reflex — every time a borrow is awkward, a move is bothersome: a little .clone() and it compiles. Each one is a silent allocation, and most often the sign of a borrow you couldn't express.

Cloner duplique le tampon ; emprunter ne fait que pointerCloning duplicates the buffer; borrowing only points
original.clone()original.clone()
original (pile)
tas: « un long texte… »
copie (pile)
tas: « un long texte… »

2 tampons sur le tas — octets dupliqués2 heap buffers — bytes duplicated

&original&original
original (pile)
tas: « un long texte… »
vue: &str (pile)
──▸pointe le même tamponpoints at the same buffer

1 seul tampon — une référence qui pointe1 buffer only — a reference that points

Ce que coûte un clone, selon le typeWhat a clone costs, by type
let original = String::from("un assez long texte…");

let copie = original.clone();   // ALLOUE un nouveau tampon sur le tas + recopie tous les octets
// `original` et `copie` sont deux String distinctes, chacune possède SON tampon.

let n: i32 = 42;
let m = n;                      // pour un type Copy, « copier » est une copie de bits — zéro tas.
// le coût de clone() dépend du type : trivial pour Copy (i32, bool), une ALLOCATION
// pour String, Vec, HashMap… — tout ce qui possède de la mémoire sur le tas.

Le mot .clone() recouvre deux réalités très différentes. Pour un type Copy (un i32, un bool, un char), il n'y a rien sur le tas : la valeur tient tout entière dans ses octets, la « copier » est un transfert de quelques octets, gratuit — d'ailleurs on écrit juste let m = n;. Pour un type qui possède du tas, c'est une allocation plus une recopie, proportionnelle à la taille des données. La distinction Copy / Clone n'est pas cosmétique : Copy est implicite et toujours trivial ; Clone est explicite parce qu'il peut être cher. Voir le .clone(), c'est voir le coût.The word .clone() covers two very different realities. For a Copy type (an i32, a bool, a char), there's nothing on the heap: the value lives entirely in its bytes, 'copying' it is a few-byte transfer, free — indeed you just write let m = n;. For a type that owns heap, it's an allocation plus a copy, proportional to the data's size. The Copy / Clone distinction isn't cosmetic: Copy is implicit and always trivial; Clone is explicit because it can be costly. Seeing the .clone() is seeing the cost.

Le réflexe du numéroThe issue's reflex

« Ce .clone() est-il un vrai besoin de posséder une seconde copie — ou un contournement du borrow checker ? » La plupart des clones qu'on ajoute pour « faire passer » le compilateur trahissent un emprunt qu'on aurait pu écrire. Le bon réflexe : devant un .clone() réflexe, se demander d'abord si un & ne suffirait pas. Cloner reste parfois la bonne réponse — mais ce doit être un choix, pas un automatisme pour calmer une erreur.'Is this .clone() a genuine need to own a second copy — or a borrow-checker workaround?' Most clones added to 'make the compiler pass' betray a borrow you could have written. The right reflex: faced with a reflexive .clone(), first ask whether a & wouldn't do. Cloning is sometimes the right answer — but it must be a choice, not an automatism to quiet an error.

Copy vs Clone, la frontièreCopy vs Clone, the boundary

Un type est Copy si le dupliquer revient à copier ses bits, sans rien à allouer ni libérer : les entiers, les flottants, les booléens, les tuples de Copy. La copie est alors implicite (let m = n laisse n utilisable) et toujours gratuite. Tout ce qui possède une ressource — un tampon sur le tas (String, Vec), un handle de fichier — n'est PAS Copy : le dupliquer demande un travail, donc un appel explicite à .clone(). C'est pourquoi Copy s'obtient par dérivation seulement quand c'est trivial, et que Clone, lui, est toujours visible dans le code.A type is Copy if duplicating it amounts to copying its bits, with nothing to allocate or free: integers, floats, booleans, tuples of Copy. The copy is then implicit (let m = n leaves n usable) and always free. Anything owning a resource — a heap buffer (String, Vec), a file handle — is NOT Copy: duplicating it requires work, hence an explicit .clone() call. That's why Copy is derivable only when trivial, and Clone is always visible in the code.

№ 02La surpriseThe surprise4 min

Le .clone() « pour faire passer » était, presque toujours, un & manquant.The .clone() 'to make it pass' was, almost always, a missing &.

Voici la surprise du numéro, et elle est presque toujours la même. On écrit une fonction qui prend un String, on l'appelle, le compilateur se plaint que la valeur est déplacée et qu'on ne peut plus s'en servir après — alors on ajoute .clone() et l'erreur disparaît. Sauf que la fonction ne faisait que lire son argument : elle n'avait jamais besoin de le posséder. Le bon geste n'était pas de dupliquer la donnée, mais de l'emprunter — prendre &str au lieu de String. Le clone qu'on a ajouté n'était pas un besoin de possession ; c'était un emprunt qu'on n'a pas su exprimer. Le compilateur ne réclamait pas une copie : il posait une question — cette fonction doit-elle posséder, ou lui suffit-il d'emprunter ? — et le .clone() réflexe est une mauvaise réponse à cette question. C'est l'écho direct du Vol 1 №3 : prends une vue, ne prends pas possession.Here's the issue's surprise, and it's almost always the same. You write a function taking a String, you call it, the compiler complains the value is moved and can't be used afterward — so you add .clone() and the error vanishes. Except the function only read its argument: it never needed to own it. The right move wasn't to duplicate the data, but to borrow it — take &str instead of String. The clone you added wasn't a need to own; it was a borrow you couldn't express. The compiler wasn't demanding a copy: it was asking a question — must this function own, or does borrowing suffice? — and the reflexive .clone() is a bad answer to it. It's the direct echo of Vol 1 №3: take a view, don't take ownership.

On clone pour garder la valeur après l'appelYou clone to keep the value after the call
fn afficher(s: String) {        // prend la POSSESSION… alors qu'il ne fait que LIRE
    println!("{s}");
}

let nom = String::from("Ada");
afficher(nom.clone());          // ✕ on clone juste pour GARDER `nom` après l'appel
println!("encore : {nom}");
// sans le clone : error[E0382] — `nom` a été déplacé dans afficher(). on a « réparé »
// par une allocation, là où le compilateur demandait en réalité de DÉCIDER la possession.

La fonction prend String par valeur, donc l'appel déplace nom ; pour pouvoir l'utiliser ensuite, on clone. Mais regarde ce que fait afficher : elle lit, point. Exiger la possession d'une donnée qu'on ne fait que lire, c'est imposer à chaque appelant de céder sa valeur ou d'en payer une copie. L'allocation du clone n'achète rien — elle compense seulement une signature trop gourmande. Le coût est réel, le bénéfice nul.The function takes String by value, so the call moves nom; to use it afterward, you clone. But look at what afficher does: it reads, period. Demanding ownership of data you only read forces every caller to give up its value or pay for a copy. The clone's allocation buys nothing — it merely compensates for a too-greedy signature. The cost is real, the benefit nil.

Le compilateur ne réclamait pas une copie — il posait une question : posséder, ou emprunter ? Le .clone() réflexe est une mauvaise réponse.The compiler wasn't demanding a copy — it was asking a question: own, or borrow? The reflexive .clone() is a bad answer.
№ 03La mécaniqueThe mechanics5 min

Cow : emprunter tant qu'on peut, n'allouer qu'à l'instant où l'on mute.Cow: borrow as long as you can, allocate only the instant you mutate.

Cloner n'est pas toujours une faute : c'est le bon geste dès qu'on a réellement besoin d'une seconde possession — une copie qu'on stocke dans une struct, qu'on envoie dans un thread ou un canal (Vol 7), qui doit vivre sa propre vie indépendamment de l'original. Le problème n'est pas clone en soi, c'est le clone réflexe, pour seulement lire. Reste un cas intermédiaire, fréquent et subtil : une fonction qui, la plupart du temps, renvoie son entrée inchangée — mais qui, parfois, doit la modifier. La forcer à toujours retourner un String possédé, c'est allouer même quand rien ne change ; la forcer à toujours emprunter, c'est s'interdire de modifier. Cow — Clone on Write — tranche : c'est un type qui tient soit un emprunt (Borrowed), soit une valeur possédée (Owned), et qui ne paie l'allocation qu'au moment exact où il faut écrire. Tant qu'on lit, il emprunte ; dès qu'on mute, il copie. On ne paie que sur la branche qui en a vraiment besoin.Cloning isn't always a fault: it's the right move the moment you genuinely need a second ownership — a copy you store in a struct, send into a thread or a channel (Vol 7), that must live its own life independently of the original. The problem isn't clone itself, it's the reflexive clone, just to read. A middle case remains, common and subtle: a function that, most of the time, returns its input unchanged — but that, sometimes, must modify it. Forcing it to always return an owned String allocates even when nothing changes; forcing it to always borrow forbids modifying. Cow — Clone on Write — decides it: a type holding either a borrow (Borrowed) or an owned value (Owned), paying the allocation only at the exact moment it must write. As long as you read, it borrows; the moment you mutate, it copies. You pay only on the branch that truly needs it.

Cow : un emprunt qui ne devient une copie qu'à l'écritureCow: a borrow that becomes a copy only on write
Cow::Borrowed(&str)
──▸ original
0 allocation · on lit0 allocation · reading
on doit muter ▸must mutate ▸clone on writeclone on write
Cow::Owned(String)
tampon propreown buffer
1 allocation · seulement ici1 allocation · only here
Cow en pratique — payer l'allocation seulement si l'on muteCow in practice — pay the allocation only if you mutate
use std::borrow::Cow;

fn sans_espaces(s: &str) -> Cow<'_, str> {
    if s.contains(' ') {
        Cow::Owned(s.replace(' ', ""))   // il a fallu modifier → on ALLOUE une String
    } else {
        Cow::Borrowed(s)                 // rien à changer → on EMPRUNTE, zéro allocation
    }
}
// le cas fréquent (déjà propre) ne coûte RIEN ; on ne paie l'allocation que sur le cas
// rare qui mute vraiment. « clone on write » : emprunter tant qu'on peut, copier au besoin.

Cow<str> est un enum à deux variantes : Borrowed(&str) et Owned(String). La fonction renvoie l'une ou l'autre selon qu'elle a dû modifier. L'appelant, lui, manipule un Cow uniforme — il peut le lire comme un &str dans les deux cas. Le gain est net quand le cas « rien à changer » domine : on évite l'allocation sur le chemin fréquent, et on ne la paie que sur l'exception. Si tu modifies presque toujours, Cow n'apporte rien — autant retourner un String directement.Cow<str> is a two-variant enum: Borrowed(&str) and Owned(String). The function returns one or the other depending on whether it had to modify. The caller manipulates a uniform Cow — it can read it as a &str in both cases. The gain is clear when the 'nothing to change' case dominates: you avoid the allocation on the frequent path, paying it only on the exception. If you almost always modify, Cow brings nothing — just return a String directly.

Quand cloner est le bon gesteWhen cloning is the right move
// cloner EST correct quand on a vraiment besoin d'une seconde possession :
let config = charger();
let pour_le_thread = config.clone();         // une copie qui part VIVRE ailleurs (Vol 7)
thread::spawn(move || utiliser(pour_le_thread));
utiliser(config);                            // l'original continue ici, en parallèle
// ici le clone n'est pas un contournement : les deux copies ont une vie propre.
// cloner est un choix légitime — c'est le clone RÉFLEXE, pour lire, qui est le problème.
clone, to_owned, to_string : la même famille.clone, to_owned, to_string: the same family.

Plusieurs noms pour des gestes voisins. .clone() duplique une valeur possédée en une autre du même type (String → String). .to_owned() part d'un emprunt et produit la version possédée (&str → String, &[T] → Vec<T>) — c'est le pont générique « emprunt → possession », celui que Cow appelle en interne quand il bascule en Owned. .to_string() est le cas particulier pour obtenir un String (via Display). Toutes font une allocation pour les types à tas : choisir le bon nom ne change pas le coût, seulement la lisibilité de l'intention.Several names for neighbouring gestures. .clone() duplicates an owned value into another of the same type (String → String). .to_owned() starts from a borrow and produces the owned version (&str → String, &[T] → Vec<T>) — it's the generic 'borrow → owned' bridge, the one Cow calls internally when it switches to Owned. .to_string() is the special case to get a String (via Display). All allocate for heap types: choosing the right name doesn't change the cost, only the readability of intent.

Cloner pour posséder vraiment est légitime ; Cow est le compromis quand on emprunte le plus souvent et ne copie qu'à l'écriture.Cloning to truly own is legitimate; Cow is the compromise when you mostly borrow and copy only on write.
№ 04Le piègeThe trap4 min

Mettre Cow partout « pour les perfs » — et payer en complexité un gain nul.Putting Cow everywhere 'for perf' — and paying in complexity for a nil gain.

Le numéro a deux faces, et la seconde est le miroir de la première. On a vu le clone réflexe, qui cache un coût ; voici le piège inverse, plus insidieux parce qu'il se croit vertueux : truffer le code de Cow « pour éviter les allocations ». Cow n'est gratuit ni à écrire ni à lire. Dès qu'il entre dans une struct, il y traîne un paramètre de lifetime, qui contamine ensuite chaque fonction touchant cette struct (le combat du Vol 2 №3, ressuscité). Le code se hérisse de 'a, de match Borrowed/Owned, de signatures alourdies. Et tout cela pour quoi ? Si les données sont en réalité toujours possédées — comme des champs lus depuis un fichier parsé —, la branche Borrowed n'arrive jamais : on a payé toute la complexité de Cow pour zéro allocation économisée. C'est l'optimisation prématurée dans sa forme la plus pure : un coût d'ingénierie certain contre un gain de performance imaginé, que nul profil n'a réclamé.The issue has two faces, and the second mirrors the first. We saw the reflexive clone, hiding a cost; here's the inverse trap, more insidious because it thinks itself virtuous: stuffing the code with Cow 'to avoid allocations'. Cow is free neither to write nor to read. The moment it enters a struct, it drags a lifetime parameter, which then contaminates every function touching that struct (Vol 2 №3's fight, resurrected). The code bristles with 'a, with match Borrowed/Owned, with heavier signatures. And all for what? If the data is in reality always owned — like fields read from a parsed file — the Borrowed branch never happens: you paid all of Cow's complexity for zero allocation saved. It's premature optimization in its purest form: a certain engineering cost against an imagined performance gain, that no profile asked for.

Trois besoins, trois outils — Cow n'est que le cas du milieuThree needs, three tools — Cow is only the middle case
toujours possédéalways owned
String
simple, sans lifetimesimple, no lifetime
toujours empruntéalways borrowed
&str
zéro copie, un lifetimezero copy, one lifetime
les deux · chemin chaudboth · hot path
Cow<str>
le seul cas qui le justifiethe only case that justifies it
Le contre-exemple — Cow là où la donnée est toujours possédéeThe counter-example — Cow where the data is always owned
// « pour éviter les allocations », on Cow-ifie tout — y compris là où c'est inutile
struct Config<'a> {
    nom: Cow<'a, str>,         // ✕ ajoute un lifetime à TOUTE la struct…
    hote: Cow<'a, str>,        //   …qui contamine chaque fonction qui touche Config
}
// mais ces champs viennent d'un fichier parsé puis possédé : ils sont TOUJOURS Owned.
// on a payé en complexité (lifetimes, match, signatures) un gain d'allocation… nul.
Le bon réflexe — l'outil le plus simple qui répond au besoinThe right reflex — the simplest tool that meets the need
struct Config {
    nom: String,               // ✓ possédé, simple, aucun lifetime à propager
    hote: String,
}
// règle : possédé en permanence → String ; emprunté en permanence → &str ;
// les DEUX, sur un chemin chaud que le profil pointe → alors, et alors seulement, Cow.

Le triage est mécanique. La donnée est-elle toujours possédée par la struct ? String. Toujours empruntée à quelqu'un qui vit plus longtemps ? &str (avec son lifetime, assumé). Et vraiment les deux — parfois emprunté, parfois possédé, sur un chemin que le profil désigne comme chaud ? Alors Cow gagne sa complexité. Le piège n'est pas Cow ; c'est de l'employer sans que les deux branches existent réellement. La simplicité par défaut, l'optimisation sur preuve.The triage is mechanical. Is the data always owned by the struct? String. Always borrowed from someone living longer? &str (with its lifetime, assumed). And genuinely both — sometimes borrowed, sometimes owned, on a path the profile flags as hot? Then Cow earns its complexity. The trap isn't Cow; it's using it without both branches really existing. Simplicity by default, optimization on proof.

Et l'autre excès : ne déclone pas par superstitionAnd the other excess: don't de-clone by superstition

Symétriquement, traquer chaque .clone() comme un péché est aussi une erreur. Un clone d'un petit Vec au démarrage, un String dupliqué une fois par requête : leur coût est invisible au regard de tout le reste. Y consacrer du temps et de la complexité, c'est gaspiller l'effort là où il ne rapporte rien. La règle de tout le Vol 9 : un clone sur un chemin froid est un non-problème ; on optimise ce qu'un profil pointe, pas ce qui ressemble à un coût.Symmetrically, hunting every .clone() like a sin is also a mistake. A clone of a small Vec at startup, a String duplicated once per request: their cost is invisible against everything else. Spending time and complexity on it wastes effort where it returns nothing. The whole of Vol 9's rule: a clone on a cold path is a non-problem; you optimize what a profile flags, not what looks like a cost.

« On n'y entre qu'avec un profil »'You enter only with a profile'

La règle d'or du volume, posée en intro de la série. Avant de remplacer un clone par un Cow, un String par un &str à grand renfort de lifetimes, ou une chaîne d'itérateurs par une boucle manuelle, on mesure. Un profileur (cargo flamegraph, perf, un simple chronométrage) dit où le temps part vraiment — et c'est presque jamais là où l'intuition le croit. Optimiser sans profil, c'est complexifier au hasard : on dégrade la lisibilité avec certitude pour un gain hypothétique. La performance se prouve, elle ne se devine pas.The volume's golden rule, set in the series' intro. Before replacing a clone with a Cow, a String with a &str amid a flurry of lifetimes, or an iterator chain with a manual loop, you measure. A profiler (cargo flamegraph, perf, a simple timing) says where the time truly goes — and it's almost never where intuition believes. Optimizing without a profile is complexifying at random: you degrade readability with certainty for a hypothetical gain. Performance is proven, not guessed.

String si l'on possède, &str si l'on emprunte, Cow seulement si les deux arrivent vraiment — et seulement si un profil le réclame.String if you own, &str if you borrow, Cow only if both genuinely happen — and only if a profile asks for it.
№ 05Bilan & éditoWrap-up & editorial2 min

Le coût qu'on ne peut pas effacer : il faut le voir, puis le choisir.The cost you can't erase: you must see it, then choose it.

Là où les itérateurs s'effaçaient à la compilation, .clone() fait un travail réel : une allocation et une recopie, pour tout ce qui possède du tas. Le réflexe de cloner « pour faire passer le compilateur » est presque toujours un emprunt qu'on n'a pas su écrire — le compilateur ne réclamait pas une copie, il demandait de décider entre posséder et emprunter. Cloner reste légitime quand on a vraiment besoin d'une seconde possession ; et Cow tranche le cas intermédiaire, en empruntant tant qu'on lit et en n'allouant qu'à l'écriture. Mais le piège guette des deux côtés : cloner partout par paresse, ou Cow-ifier partout par zèle — payer la complexité d'une optimisation que nul profil n'a réclamée. La ligne du volume tient en une phrase : on n'optimise qu'avec un profil, jamais avec une intuition.Where iterators erased themselves at compile time, .clone() does real work: an allocation and a copy, for everything owning heap. The reflex of cloning 'to make the compiler pass' is almost always a borrow you couldn't express — the compiler wasn't demanding a copy, it was asking you to decide between owning and borrowing. Cloning stays legitimate when you genuinely need a second ownership; and Cow decides the middle case, borrowing as long as you read and allocating only on write. But the trap lurks on both sides: cloning everywhere out of laziness, or Cow-ifying everywhere out of zeal — paying the complexity of an optimization no profile asked for. The volume's line fits in one sentence: you optimize only with a profile, never with a hunch.

L'outilThe toolSon coût, et quand le choisirIts cost, and when to choose it
&str / &[T]Emprunter : zéro copie. Le défaut sain quand on ne fait que lire.Borrow: zero copy. The healthy default when you only read.
.clone()Allouer + recopier. Légitime pour une vraie seconde possession.Allocate + copy. Legitimate for a genuine second ownership.
Cow<str>Emprunte, copie seulement à l'écriture. Si les deux branches existent.Borrows, copies only on write. If both branches exist.
CopyCopyCopie de bits, gratuite. i32, bool… : pas de tas, pas de souci.Bitwise copy, free. i32, bool…: no heap, no worry.
le profilthe profileL'arbitre : on n'optimise que ce qu'une mesure pointe.The arbiter: you optimize only what a measurement flags.
Un .clone() visible vaut mieux qu'un coût caché : Rust te fait voir l'allocation — à toi de décider si tu la veux.A visible .clone() beats a hidden cost: Rust makes you see the allocation — yours to decide if you want it.
Mot de l'éditeurFrom the editor

On a vu le coût, et appris à le choisir. Reste l'ultime sortie : désactiver la garde.We've seen the cost, and learned to choose it. One last exit remains: disabling the guard.

Tout ce volume a parlé de coût mesuré : l'abstraction qui s'efface, l'allocation qu'on voit, l'optimisation qu'on prouve. À chaque fois, le compilateur restait le garant — il vérifiait, il refusait, il garantissait. Mais il existe un dernier recours, le plus lourd de la série, celui par lequel on sort de sa protection : unsafe. C'est la porte qu'on pousse quand on doit faire quelque chose que le compilateur ne peut pas vérifier — parler à du code C, déréférencer un pointeur brut, construire une abstraction dont la sûreté dépend d'un invariant qu'aucun type ne capture. Et c'est aussi la porte que le débutant pousse trop tôt, par réflexe, pour faire taire un combat de borrow checker qu'un meilleur design aurait réglé.This whole volume spoke of measured cost: the abstraction that erases itself, the allocation you see, the optimization you prove. Each time, the compiler stayed the guarantor — it checked, it refused, it guaranteed. But one last resort exists, the heaviest of the series, the one by which you step out of its protection: unsafe. It's the door you push when you must do something the compiler can't verify — talk to C code, dereference a raw pointer, build an abstraction whose safety rests on an invariant no type captures. And it's also the door the beginner pushes too early, by reflex, to silence a borrow-checker fight a better design would have settled.

Vol 9 №3 : unsafe & FFI — le dernier numéro de la série. On y verra le contre-pied qui résume tout : unsafe ne « désactive » pas les règles de Rust, il en transfère la preuve du compilateur vers toi. Le contrat que le compilateur tenait, c'est désormais toi qui le signes — et qui réponds de l'UB si tu le violes. On verra pourquoi le bon unsafe est rare, encapsulé derrière une API sûre dont on peut prouver les invariants, et jamais éparpillé pour contourner une erreur mal comprise. La boucle se referme là où elle a commencé : au Vol 1, la possession. unsafe est le moment où l'on reprend, à la main, la preuve qu'elle nous offrait.Vol 9 №3: unsafe & FFI — the series' final issue. We'll see the reversal that sums everything up: unsafe doesn't 'disable' Rust's rules, it transfers their proof from the compiler to you. The contract the compiler held, you now sign — and answer for the UB if you break it. We'll see why good unsafe is rare, encapsulated behind a safe API whose invariants can be proven, and never scattered to work around a misunderstood error. The loop closes where it began: at Vol 1, ownership. unsafe is the moment you take back, by hand, the proof it offered you.