Vol. 6 · № 01 · Un atelier RustA Rust workshop

Box<T>, ou la possession unique déportée Box<T>, or single ownership relocated

Taille connue, types récursifs, possession unique d'une donnée déportée sur le tas. La première échappatoire à l'arbre de possession — la plus simple, la moins chère. Known size, recursive types, single ownership of data relocated to the heap. The first escape from the ownership tree — the simplest, the cheapest.

AudienceAudience
Dev maîtrisant possession et durées de vie (Vol 1–2) Dev fluent in ownership and lifetimes (Vol 1–2)
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●●○ Smart pointersBoxTas
SommaireContents ·
№ 01CadrageFraming3 min

La même possession unique — mais la donnée vit ailleurs, sur le tas.The same single ownership — but the data lives elsewhere, on the heap.

Le Vol 1 a posé la possession comme un arbre : un propriétaire par donnée, libérée quand son propriétaire disparaît. Ce volume montre les échappatoires quand l'arbre strict ne suffit pas — et leur prix. Box<T> est la première, la plus simple, et celle qui ne quitte presque pas l'arbre. Une Box ne change rien à la règle « un seul propriétaire » : elle déplace seulement la donnée de la pile vers le tas, et garde sur la pile un pointeur vers elle. Pourquoi le voudrait-on ? Parce que certaines choses n'ont pas de taille connue à la compilation, ou n'en ont pas du tout sur la pile : un type récursif, un trait object, une valeur énorme qu'on ne veut pas copier en bloc. Box leur donne une adresse fixe sur le tas, et une taille fixe — celle d'un pointeur — sur la pile.Vol 1 set ownership as a tree: one owner per piece of data, freed when its owner goes away. This volume shows the escapes when the strict tree falls short — and their price. Box<T> is the first, the simplest, and the one that barely leaves the tree at all. A Box changes nothing about the 'single owner' rule: it merely moves the data from the stack to the heap, and keeps on the stack a pointer to it. Why would you want that? Because some things have no compile-time size, or none on the stack at all: a recursive type, a trait object, a huge value you don't want to copy wholesale. Box gives them a fixed address on the heap, and a fixed size — that of a pointer — on the stack.

Box<T> — un pointeur sur la pile, la valeur sur le tasBox<T> — a pointer on the stack, the value on the heap
// Box<T> : la valeur vit sur le TAS, un pointeur la tient sur la PILE
let sur_la_pile: i32 = 42;            // 4 octets, directement sur la pile
let sur_le_tas: Box<i32> = Box::new(42);  // un pointeur sur la pile → 42 sur le tas

println!("{}", *sur_le_tas);          // * déréférence : on lit la valeur pointée

// la possession reste UNIQUE et l'arbre intact : un seul propriétaire (la Box),
// libération automatique à la fin de portée. on a juste déporté la donnée.
Pile et tas, en un coup d'œilStack and heap, at a glance
PILESTACK
taille fixe, rapidefixed size, fast
sur_la_pile42
sur_le_tas●──┐
──→
TASHEAP
alloué à la demandeallocated on demand
(anonyme)(anonymous)42

La pile est rapide, automatique, à taille fixe connue à la compilation : c'est là que vivent par défaut tes variables. Le tas est une zone plus libre où l'on alloue à la demande, pour ce dont la taille n'est pas connue à l'avance ou doit survivre à un cadre de pile. Box::new(v) alloue sur le tas, y déplace v, et te rend un pointeur. Quand la Box sort de portée, sa donnée sur le tas est libérée — automatiquement, sans ramasse-miettes, par le même mécanisme de possession que tout le reste.The stack is fast, automatic, of fixed compile-time size: that's where your variables live by default. The heap is a freer area where you allocate on demand, for things whose size isn't known ahead or must outlive a stack frame. Box::new(v) allocates on the heap, moves v there, and hands you a pointer. When the Box goes out of scope, its heap data is freed — automatically, no garbage collector, by the same ownership mechanism as everything else.

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

« Ai-je une raison précise de mettre cette valeur sur le tas ? » Trois bonnes raisons : un type récursif (qui se contient lui-même), un type non dimensionné (un dyn Trait, vu au Vol 5), ou une valeur si grosse que la déplacer sur la pile coûte cher. Hors de ces cas, la pile suffit — et Box n'ajouterait qu'une allocation inutile.'Do I have a precise reason to put this value on the heap?' Three good ones: a recursive type (one that contains itself), an unsized type (a dyn Trait, from Vol 5), or a value so large that moving it on the stack is costly. Outside these, the stack suffices — and Box would only add a needless allocation.

Le smart pointer le plus simple.The simplest smart pointer.

Box est le premier des « smart pointers » : des types qui se comportent comme un pointeur mais gèrent une ressource. Box ne fait qu'une chose — posséder une donnée sur le tas et la libérer à la fin. Pas de comptage, pas de partage, pas de vérification à l'exécution. C'est pour ça qu'il ne coûte presque rien et qu'il ne quitte pas vraiment l'arbre de possession : un seul propriétaire, comme avant. Les numéros suivants — Rc, RefCell — ajouteront le partage et la mutabilité intérieure, avec leur prix.Box is the first of the 'smart pointers': types that behave like a pointer but manage a resource. Box does just one thing — own a piece of data on the heap and free it at the end. No counting, no sharing, no runtime check. That's why it costs almost nothing and doesn't really leave the ownership tree: a single owner, as before. The next issues — Rc, RefCell — will add sharing and interior mutability, with their price.

№ 02La surpriseThe surprise4 min

Un type qui se contient lui-même a une taille infinie — sauf derrière un pointeur.A type that contains itself has infinite size — except behind a pointer.

Voici le cas d'école qui rend Box indispensable. Pour poser une valeur sur la pile, le compilateur doit connaître sa taille exacte. Or un type récursif — une liste chaînée, un arbre — se contient lui-même : pour connaître la taille d'un maillon, il faudrait connaître la taille du maillon suivant, qui contient le suivant… à l'infini. Le compilateur refuse, avec l'erreur « infinite size ». La solution n'est pas de renoncer à la récursion, mais d'y insérer une indirection : un Box. Un pointeur a une taille fixe — un mot machine — quel que soit ce qu'il pointe. Remplacer « contient une Liste » par « contient un pointeur vers une Liste » rend la taille finie, et la récursion possible. Le tas devient le lieu où la structure peut grandir sans que la pile ait à la mesurer.Here's the textbook case that makes Box indispensable. To place a value on the stack, the compiler must know its exact size. But a recursive type — a linked list, a tree — contains itself: to know a node's size, you'd need the next node's size, which contains the next… infinitely. The compiler refuses, with the 'infinite size' error. The solution isn't to give up recursion, but to insert an indirection into it: a Box. A pointer has a fixed size — one machine word — whatever it points to. Replacing 'contains a Liste' with 'contains a pointer to a Liste' makes the size finite, and recursion possible. The heap becomes the place where the structure can grow without the stack having to measure it.

Le mur — la récursion par valeur ne se mesure pasThe wall — recursion by value can't be measured
// Une liste chaînée « naïve » : chaque maillon contient le suivant… directement
enum Liste {
    Cons(i32, Liste),   // un i32, PUIS une autre Liste — par valeur
    Nil,
}

// error[E0072]: recursive type `Liste` has infinite size
//   pour calculer la taille de Liste, il faut la taille de Liste, qui contient
//   une Liste, qui contient une Liste… la récursion ne s'arrête jamais.
Cons(1, Liste)
⊃
Cons(2, Liste)
⊃
Cons(…, Liste)
⊃ ∞✕ ne s'arrête jamais✕ never stops
On ne met pas la récursion sur le tas par goût : on y met un pointeur de taille fixe pour qu'un type qui se contient cesse d'être infini.You don't put recursion on the heap by taste: you put a fixed-size pointer there so a self-containing type stops being infinite.
№ 03La mécaniqueThe mechanics5 min

Déplacer une Box, c'est déplacer un pointeur — pas la donnée pointée.Moving a Box means moving a pointer — not the data it points to.

Une Box obéit à toutes les règles du Vol 1 — elle a un propriétaire, se déplace par move, se libère à la fin de portée — mais avec une nuance qui la rend précieuse. Comme la donnée vit sur le tas et que la Box n'en détient qu'un pointeur, déplacer la Box ne copie que le pointeur : quelques octets, quelle que soit la taille de la valeur. Déplacer une structure d'un mégaoctet posée sur la pile recopierait un mégaoctet ; la même valeur dans une Box se déplace en copiant huit octets. L'accès se fait par déréférencement, souvent automatique grâce au trait Deref, si bien qu'une Box s'utilise presque comme la valeur elle-même. Et quand elle meurt, son implémentation de Drop rend la mémoire du tas — automatiquement, sans ramasse-miettes, exactement comme pour une valeur de pile.A Box obeys all of Vol 1's rules — it has an owner, moves by move, frees at end of scope — but with a nuance that makes it precious. Since the data lives on the heap and the Box holds only a pointer to it, moving the Box copies only the pointer: a few bytes, whatever the value's size. Moving a one-megabyte struct sitting on the stack would recopy a megabyte; the same value in a Box moves by copying eight bytes. Access happens through dereferencing, often automatic thanks to the Deref trait, so a Box is used almost like the value itself. And when it dies, its Drop implementation returns the heap memory — automatically, no garbage collector, exactly as for a stack value.

Le move ne copie que le pointeurThe move copies only the pointer
let a = Box::new([0u8; 1_000_000]);   // 1 Mo sur le TAS, un pointeur sur la pile
let b = a;                            // move : on copie le POINTEUR (8 octets),
                                      // PAS le mégaoctet. a est invalidé (Vol 1).
// déplacer une Box est toujours bon marché, quelle que soit la taille pointée.
Deref pour lire, Drop pour libérerDeref to read, Drop to free
let boite = Box::new(Point { x: 1, y: 2 });

let px = boite.x;        // Deref automatique : boite.x ≡ (*boite).x
afficher(&*boite);       // *boite emprunte la valeur sur le tas

// à la fin de portée, Drop de Box libère le tas — automatiquement :
//   { let b = Box::new(gros_buffer); }   // ← ici, le tas est rendu, sans GC

Deux traits font tout le travail. Deref — celui-là même qu'on déconseillait sur un newtype au Vol 5 — est ici parfaitement à sa place : Box est un pointeur intelligent, et Deref lui donne sa transparence d'accès (boite.x traverse le pointeur tout seul). Drop libère le tas quand la Box sort de portée. Tu n'appelles jamais free : la possession s'en charge, déterministe, au point exact où le propriétaire disparaît. C'est la gestion mémoire sans GC du Vol 1, étendue au tas sans rien changer au modèle mental.Two traits do all the work. Deref — the very one we warned against on a newtype in Vol 5 — is perfectly at home here: Box is a smart pointer, and Deref gives it its access transparency (boite.x crosses the pointer on its own). Drop frees the heap when the Box goes out of scope. You never call free: ownership handles it, deterministically, at the exact point the owner disappears. It's Vol 1's GC-free memory management, extended to the heap with no change to the mental model.

Box<dyn>, le pont avec le Vol 5Box<dyn>, the bridge to Vol 5

Tu as déjà croisé Box au Vol 5, sans qu'on l'explique : Box<dyn Trait>. La raison est exactement celle de ce numéro — un dyn Trait n'a pas de taille connue (le type concret est effacé), il lui faut donc l'indirection d'un pointeur pour exister sur la pile. Box est le moyen le plus simple de posséder un trait object. Type récursif, type effacé : deux visages du même besoin — donner une taille fixe à ce qui n'en a pas.You already met Box in Vol 5, unexplained: Box<dyn Trait>. The reason is exactly this issue's — a dyn Trait has no known size (the concrete type is erased), so it needs a pointer's indirection to exist on the stack. Box is the simplest way to own a trait object. Recursive type, erased type: two faces of the same need — giving a fixed size to what has none.

Le coût, mesuré.The cost, measured.

Box a un prix, modeste mais réel : une allocation sur le tas à la création, une libération à la fin, et une indirection à chaque accès (suivre le pointeur). L'allocation tas est plus lente que poser sur la pile, et la donnée déportée nuit à la localité de cache. Rien de comparable au comptage de Rc ou au verrou de Mutex à venir — mais ce n'est pas gratuit. D'où le réflexe : Box quand on en a besoin, pas par habitude.Box has a price, modest but real: a heap allocation at creation, a free at the end, and an indirection on each access (follow the pointer). Heap allocation is slower than pushing on the stack, and the relocated data hurts cache locality. Nothing like the coming Rc's counting or Mutex's lock — but not free. Hence the reflex: Box when you need it, not by habit.

Type récursif ou type effacé, même réponse : donner une taille fixe à ce qui n'en a pas, derrière un pointeur.Recursive type or erased type, same answer: give a fixed size to what has none, behind a pointer.
№ 04Le piègeThe trap4 min

Le Box par réflexe : une allocation et une indirection pour rien.The reflexive Box: an allocation and an indirection for nothing.

Qui vient d'un langage où « tout est sur le tas » a le réflexe d'emballer par défaut. En Rust, c'est l'inverse : la pile est le lieu naturel, et Box l'exception qu'on justifie. Mettre dans une Box une valeur de petite taille connue — un entier, un booléen — coûte plus cher que la valeur elle-même : on paie une allocation tas et une indirection à chaque accès, pour déplacer deux octets. Pire, on emballe parfois des types qui possèdent déjà leur tas : un String, un Vec gèrent leur propre tampon ; les mettre dans une Box ajoute une seconde indirection inutile. Et un Vec<Box<T>> sacrifie la contiguïté qui fait la vitesse du Vec, échangeant un bloc compact contre une myriade d'allocations éparpillées. Le Box gratuit ne résout aucun problème : il en crée un de performance.Anyone from a language where 'everything is on the heap' reflexively boxes by default. In Rust it's the opposite: the stack is the natural place, and Box the exception you justify. Putting a small, known-size value — an integer, a boolean — in a Box costs more than the value itself: you pay a heap allocation and an indirection on each access, to move two bytes. Worse, you sometimes box types that already own their heap: a String, a Vec manage their own buffer; putting them in a Box adds a needless second indirection. And a Vec<Box<T>> sacrifices the contiguity that makes a Vec fast, trading a compact block for a scattering of allocations. The gratuitous Box solves no problem: it creates a performance one.

Le contre-exemple — emballer ce qui n'a pas à l'êtreThe counter-example — boxing what needn't be
// Box par habitude, sur des valeurs qui n'en ont aucun besoin
struct Config {
    port: Box<u16>,          // ✕ un u16 fait 2 octets : le Box coûte plus que la valeur
    nom: Box<String>,        // ✕ String possède DÉJÀ son tampon sur le tas — double indirection
}

let nombres: Vec<Box<i32>> = vec![Box::new(1), Box::new(2), Box::new(3)];
// ✕ un Vec<i32> range déjà ses éléments de façon contiguë sur le tas.
// chaque Box ajoute une allocation séparée et un saut de pointeur, pour RIEN.

Box<u16> stocke sur le tas une valeur plus petite que le pointeur qui la désigne — un comble. Box<String> est une double indirection : le pointeur de la Box mène à un String, qui contient lui-même un pointeur vers son tampon. Et Vec<Box<i32>> disperse en N allocations ce qu'un Vec<i32> range en un seul bloc contigu, ruinant la localité de cache. Aucun de ces Box ne répond à un type récursif ou effacé : ils ne font que coûter.Box<u16> stores on the heap a value smaller than the pointer that designates it — absurd. Box<String> is a double indirection: the Box's pointer leads to a String, which itself holds a pointer to its buffer. And Vec<Box<i32>> scatters across N allocations what a Vec<i32> stores in one contiguous block, wrecking cache locality. None of these Boxes answers a recursive or erased type: they only cost.

Le bon réflexe — la pile par défaut, Box quand le type l'exigeThe right reflex — the stack by default, Box when the type demands it
// On laisse sur la pile / dans le conteneur ce qui n'a pas besoin du tas
struct Config {
    port: u16,               // ✓ directement, 2 octets
    nom: String,             // ✓ String gère son propre tas, inutile de l'emballer
}

let nombres: Vec<i32> = vec![1, 2, 3];   // ✓ contigu, une seule allocation, cache-friendly

// Box se réserve aux cas qui l'exigent vraiment :
struct Arbre { valeur: i32, gauche: Option<Box<Arbre>>, droite: Option<Box<Arbre>> }
let handlers: Vec<Box<dyn Fn()>> = vec![];   // types effacés, tailles différentes

Les champs simples restent nus ; String et Vec gèrent leur tas tout seuls. Box ne réapparaît que là où il est structurellement nécessaire : l'Arbre récursif (sans Box, taille infinie), la collection de closures de types différents (dyn Fn, tailles hétérogènes). Là, le Box n'est pas un coût gratuit mais la condition d'existence du type. La règle est celle de toute la série : l'outil se mérite par un besoin réel, pas par habitude.Simple fields stay bare; String and Vec manage their heap on their own. Box reappears only where it's structurally necessary: the recursive Arbre (without Box, infinite size), the collection of differently-typed closures (dyn Fn, heterogeneous sizes). There, the Box isn't a free cost but the type's condition of existence. The rule is the whole series': the tool is earned by a real need, not by habit.

Le test mentalThe mental test

« Si je retire ce Box, qu'est-ce qui casse ? » Rien ne compile en moins, juste « ça marche » → le Box était inutile, enlève-le. « Taille infinie » (récursion) ou « size not known at compile time » (dyn, slice) → le Box gagne sa place. Comme pour dyn au Vol 5 : le besoin doit casser la version sans l'outil pour justifier l'outil.'If I remove this Box, what breaks?' Nothing compiles less, it just 'works' → the Box was useless, drop it. 'Infinite size' (recursion) or 'size not known at compile time' (dyn, slice) → the Box earns its place. As with dyn in Vol 5: the need must break the version without the tool to justify the tool.

Une exception : le grand move.One exception: the big move.

Il existe un cas où Box se justifie hors récursion et hors dyn : une valeur énorme qu'on déplace souvent. Déplacer une grosse structure de pile recopie tous ses octets à chaque move ; la même dans une Box ne déplace qu'un pointeur. Si le profilage montre des moves coûteux d'une grande valeur, l'emballer peut accélérer. Mais c'est une optimisation à mesurer, pas un réflexe — la grande majorité des valeurs n'en ont aucun besoin.There's one case where Box is justified outside recursion and dyn: a huge value moved often. Moving a large stack struct recopies all its bytes on each move; the same in a Box moves only a pointer. If profiling shows costly moves of a large value, boxing it can speed things up. But it's an optimization to measure, not a reflex — the vast majority of values need nothing of the sort.

En Rust, la pile est le défaut et Box l'exception : un Box qu'on peut retirer sans rien casser n'aurait jamais dû être écrit.In Rust, the stack is the default and Box the exception: a Box you can remove without breaking anything should never have been written.
№ 05Bilan & éditoWrap-up & editorial2 min

Déporter sur le tas, sans quitter l'arbre : un propriétaire, une indirection.Relocate to the heap, without leaving the tree: one owner, one indirection.

Ce numéro ouvre le Vol 6 par l'échappatoire la plus douce. Box<T> ne touche pas à la règle du Vol 1 — un seul propriétaire — ; il déplace seulement la donnée vers le tas et en garde un pointeur sur la pile. De là, trois usages qui justifient le détour : donner une taille finie à un type récursif, une taille fixe à un type effacé (dyn), ou un déplacement bon marché à une valeur énorme. Le mécanisme reste celui qu'on connaît : Deref pour lire, Drop pour libérer, move pour transférer — sans GC, déterministe. Et le piège, fidèle au fil de la série : ne pas emballer ce que la pile gère déjà très bien. Box est l'exception, pas le défaut.This issue opens Vol 6 with the gentlest escape. Box<T> doesn't touch Vol 1's rule — a single owner — ; it merely moves the data to the heap and keeps a pointer to it on the stack. From there, three uses justify the detour: giving a finite size to a recursive type, a fixed size to an erased type (dyn), or a cheap move to a huge value. The mechanism stays the one we know: Deref to read, Drop to free, move to transfer — no GC, deterministic. And the trap, true to the series' thread: don't box what the stack already handles fine. Box is the exception, not the default.

Le besoinThe needPourquoi BoxWhy Box
Type récursifRecursive typeSans indirection, taille infinie (E0072) ; le pointeur la borne.Without indirection, infinite size (E0072); the pointer bounds it.
Type effacé (dyn)Erased type (dyn)Un dyn Trait n'a pas de taille ; Box lui en donne une fixe.A dyn Trait has no size; Box gives it a fixed one.
Grosse valeur déplacéeLarge value, moved oftenLe move copie le pointeur, pas les octets — à mesurer.The move copies the pointer, not the bytes — measure it.
Petite valeur / String, VecSmall value / String, VecAucun besoin : pile, ou tas déjà géré. Box = coût pur.No need: stack, or heap already managed. Box = pure cost.
Box ne quitte pas l'arbre de possession : il en déporte une branche sur le tas, propriétaire unique inchangé.Box doesn't leave the ownership tree: it relocates a branch to the heap, single owner unchanged.
Mot de l'éditeurFrom the editor

Un propriétaire suffisait. Et quand il en faut plusieurs ?One owner was enough. And when you need several?

Box a déporté la donnée, mais n'a rien changé au nombre de propriétaires : toujours un seul. C'est ce qui le garde si simple et si bon marché. Or certaines structures ne sont pas des arbres. Un graphe où deux nœuds pointent le même voisin ; un cache qu'un service et son journal consultent tous deux ; une donnée partagée dont on ne sait pas, à la compilation, qui la libérera en dernier. L'arbre à propriétaire unique ne peut pas les exprimer — il faudrait que la même donnée ait deux parents, ce que le Vol 1 interdit catégoriquement.Box relocated the data, but changed nothing about the number of owners: still one. That's what keeps it so simple and so cheap. Yet some structures aren't trees. A graph where two nodes point at the same neighbor; a cache that a service and its logger both consult; shared data where you can't tell, at compile time, who will free it last. The single-owner tree can't express them — the same data would need two parents, which Vol 1 categorically forbids.

Vol 6 №2 : Rc et Arc, la possession partagée. On remplace « un propriétaire » par « plusieurs, et la donnée vit tant qu'il en reste au moins un ». Le mécanisme est un compteur de références : chaque clone l'incrémente, chaque destruction le décrémente, et la libération a lieu quand il tombe à zéro. C'est la première vraie sortie de l'arbre — et le premier vrai prix : un coût à l'exécution, là où Box n'avait qu'une indirection. Rc pour un seul thread, Arc pour plusieurs, avec un surcoût d'atomicité. On entre dans le territoire où la sûreté ne se prouve plus seulement à la compilation, mais se paie aussi en cycles.Vol 6 №2: Rc and Arc, shared ownership. We replace 'one owner' with 'several, and the data lives as long as at least one remains'. The mechanism is a reference count: each clone increments it, each destruction decrements it, and freeing happens when it hits zero. It's the first real exit from the tree — and the first real price: a runtime cost, where Box had only an indirection. Rc for a single thread, Arc for several, with an atomicity overhead. We enter the territory where safety is no longer proven at compile time alone, but also paid in cycles.