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

Rc / Arc, ou le partage par comptage de références Rc / Arc, or sharing by reference counting

Plusieurs propriétaires par comptage de références — et les cycles qui fuient : la fuite mémoire que Rust n'empêche pas. Weak comme issue. Multiple owners by reference counting — and the cycles that leak: the memory leak Rust does not prevent. Weak as the way out.

AudienceAudience
Dev maîtrisant possession et Box (Vol 1, Vol 6 №1) Dev fluent in ownership and Box (Vol 1, Vol 6 №1)
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●●○ Smart pointersRcArc
SommaireContents ·
№ 01CadrageFraming3 min

Plusieurs propriétaires pour une donnée — tant qu'il en reste un, elle vit.Several owners for one piece of data — as long as one remains, it lives.

Box a déporté la donnée sur le tas sans toucher à la règle d'or : un seul propriétaire. Mais 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 dont on ne sait pas, à la compilation, qui la libérera en dernier. L'arbre du Vol 1 ne peut pas l'exprimer — il faudrait deux parents pour une même donnée. Rc<T> (reference counted) lève cette limite : il autorise plusieurs propriétaires d'une même valeur, et tient un compteur de ceux-ci. Chaque clone l'incrémente, chaque destruction le décrémente, et la donnée est libérée quand il atteint zéro. La possession unique devient une possession partagée — comptée.Box relocated the data to the heap without touching the golden rule: a single owner. But 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; data where you can't tell, at compile time, who will free it last. Vol 1's tree can't express it — it would need two parents for one piece of data. Rc<T> (reference counted) lifts that limit: it allows several owners of one value, and keeps a count of them. Each clone increments it, each destruction decrements it, and the data is freed when it reaches zero. Single ownership becomes shared ownership — counted.

Rc — un clone n'est pas une copie, c'est un propriétaire de plusRc — a clone isn't a copy, it's one more owner
use std::rc::Rc;

let donnee = Rc::new(Config { /* ... */ });   // compteur = 1
let a = Rc::clone(&donnee);                    // compteur = 2 — PAS une copie
let b = Rc::clone(&donnee);                    // compteur = 3

// trois propriétaires, une seule donnée sur le tas.
// elle vit tant qu'il reste au moins un Rc ; libérée quand le compteur tombe à 0.
println!("propriétaires : {}", Rc::strong_count(&donnee));  // 3
Trois propriétaires, une donnéeThree owners, one piece of data
donnee: Rc
a: Rc
b: Rc
──→
Config
sur le tason the heap
strong = 3

Chaque Rc est un propriétaire à part entière : aucun n'est « le vrai », aucun n'est un simple emprunt. Ils pointent tous la même valeur sur le tas, à côté de laquelle vit un compteur. Tant que ce compteur est positif, la donnée existe ; le dernier Rc à disparaître la fait passer à zéro, et c'est lui qui déclenche la libération. Personne n'a à savoir qui sera le dernier — le comptage le détermine à l'exécution. C'est exactement ce que l'arbre à propriétaire unique ne pouvait pas faire.Each Rc is a full owner: none is 'the real one', none is a mere borrow. They all point at the same heap value, beside which lives a counter. As long as that counter is positive, the data exists; the last Rc to disappear brings it to zero, and it triggers the free. No one needs to know who'll be last — the count determines it at runtime. That's exactly what the single-owner tree couldn't do.

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

« Cette donnée a-t-elle vraiment plusieurs propriétaires, ou un seul avec des vues temporaires ? » Des emprunts (&T) suffisent si un propriétaire clair survit aux vues. Rc ne se justifie que lorsqu'aucun propriétaire unique ne se dégage — quand plusieurs parties doivent garder la donnée en vie, indépendamment les unes des autres.'Does this data really have several owners, or one with temporary views?' Borrows (&T) suffice if a clear owner outlives the views. Rc is justified only when no single owner emerges — when several parts must keep the data alive, independently of one another.

Rc, c'est un seul thread.Rc is single-thread.

Rc n'est pas partageable entre threads : son compteur est une variable ordinaire, qu'incrémenter depuis deux threads à la fois corromprait. Pour le partage entre threads, il existe Arc (atomically reference counted), au compteur atomique — le sujet de la mécanique, plus loin. Le compilateur t'empêche de te tromper : un Rc envoyé à un autre thread ne compile pas. Choisir le bon, c'est répondre à une question — un thread, ou plusieurs ?Rc isn't shareable across threads: its counter is an ordinary variable, which incrementing from two threads at once would corrupt. For cross-thread sharing there's Arc (atomically reference counted), with an atomic counter — the mechanics' subject, further on. The compiler stops you from getting it wrong: an Rc sent to another thread won't compile. Choosing the right one answers a question — one thread, or several?

№ 02La surpriseThe surprise4 min

Rc::clone ne copie pas la donnée — il ajoute un propriétaire.Rc::clone doesn't copy the data — it adds an owner.

Avec la possession unique, confier une donnée à deux structures est impossible : la première la prend par move, la seconde se heurte à un E0382. On pourrait la cloner en profondeur — mais on obtiendrait alors deux copies indépendantes, qui divergeraient à la première modification. Ce n'est pas ce qu'on veut : on veut une seule donnée, vue par deux. Rc le permet, et c'est là qu'est la surprise pour qui débute : Rc::clone porte le mot « clone », mais ne duplique rien. Il incrémente un compteur et te rend un second pointeur vers la même valeur. L'opération est en temps constant, quelle que soit la taille de la donnée — partager un arbre d'un million de nœuds coûte exactement le même incrément que partager un entier.With single ownership, handing one piece of data to two structures is impossible: the first takes it by move, the second hits an E0382. You could deep-clone it — but then you'd get two independent copies, diverging at the first change. That's not what you want: you want one piece of data, seen by two. Rc allows it, and that's where the surprise lies for beginners: Rc::clone carries the word 'clone', but duplicates nothing. It increments a counter and hands you a second pointer to the same value. The operation is constant-time, whatever the data's size — sharing a million-node tree costs exactly the same increment as sharing an integer.

Le mur — un seul propriétaire, le second appel échoueThe wall — a single owner, the second use fails
// Possession unique : donner la donnée à l'un, c'est la retirer à l'autre
let arbre = Arbre::new();
let service = Service { arbre };       // arbre est MOVE dans service
let journal = Journal { arbre };       // ✕ error[E0382]: use of moved value
//                       ^^^^^ arbre a déjà été déplacé dans service

// l'arbre ne peut avoir qu'UN propriétaire. impossible de le confier à deux
// structures à la fois — sans le cloner en profondeur (coûteux, et désynchronisé).

Le move du Vol 1 est intraitable : une fois arbre donné à service, il n'existe plus pour journal. La seule échappatoire sans Rc serait un clone profond — recopier tout l'arbre. Mais alors deux arbres distincts existeraient : modifier celui du service ne changerait pas celui du journal. Quand on veut une donnée partagée, pas dupliquée, la possession unique est une impasse.The move from Vol 1 is unyielding: once arbre is given to service, it no longer exists for journal. The only escape without Rc would be a deep clone — recopying the whole tree. But then two distinct trees would exist: changing the service's wouldn't change the logger's. When you want shared data, not duplicated, single ownership is a dead end.

Rc::clone est en temps constant : il copie un pointeur et incrémente un entier, jamais la donnée — fût-elle énorme.Rc::clone is constant-time: it copies a pointer and bumps an integer, never the data — however huge.
№ 03La mécaniqueThe mechanics5 min

Un bloc de contrôle sur le tas : la valeur, et deux compteurs.A control block on the heap: the value, and two counters.

Derrière Rc, une seule allocation sur le tas regroupe trois choses : la valeur partagée, un compteur « fort » (strong) et un compteur « faible » (weak). Les Rc qui possèdent la donnée comptent dans le strong ; on verra les weak au piège. Chaque Rc::clone incrémente le strong, chaque Rc détruit le décrémente. Lorsque le strong atteint zéro, plus personne ne possède la valeur : son destructeur s'exécute, et la donnée est libérée — une seule fois, déterministe, sans ramasse-miettes. Tout le mécanisme tient dans cet entier partagé. Sa seule subtilité est concurrentielle : si plusieurs threads peuvent cloner et détruire en même temps, l'incrément doit être atomique — et c'est la différence entre Rc et Arc.Behind Rc, a single heap allocation groups three things: the shared value, a 'strong' counter and a 'weak' counter. The Rcs that own the data count in the strong; we'll see the weak in the trap. Each Rc::clone increments the strong, each dropped Rc decrements it. When the strong reaches zero, no one owns the value anymore: its destructor runs, and the data is freed — exactly once, deterministically, with no garbage collector. The whole mechanism fits in that shared integer. Its only subtlety is concurrency: if several threads can clone and drop at once, the increment must be atomic — and that's the difference between Rc and Arc.

Le bloc de contrôle — une allocation, trois champsThe control block — one allocation, three fields
a: Rc
b: Rc
──→
bloc de contrôle (tas)control block (heap)
strong2propriétaires RcRc owners
weak0références faiblesweak references
valueString("partagé")la donnéethe data
Le compteur monte et descend avec les portéesThe counter rises and falls with scopes
let a = Rc::new(String::from("partagé"));   // strong = 1
{
    let b = Rc::clone(&a);                    // strong = 2
    println!("{}", Rc::strong_count(&a));     // 2
}                                             // b sort de portée → strong = 1
println!("{}", Rc::strong_count(&a));         // 1
// quand a disparaîtra à son tour → strong = 0 → la String est libérée, une fois.

Le comptage suit exactement la possession du Vol 1, étendue au partage : un Rc qui sort de portée décrémente, comme une valeur ordinaire se libère. La différence est qu'ici la donnée ne meurt pas au premier drop, mais au dernier. Tu n'écris jamais de libération : le compteur la déclenche, au moment exact où le dernier propriétaire s'en va. La déterminisme du Vol 1 est préservé — simplement, « le propriétaire » est devenu « le dernier des propriétaires ».Counting follows exactly Vol 1's ownership, extended to sharing: an Rc going out of scope decrements, as an ordinary value frees itself. The difference is that here the data doesn't die at the first drop, but at the last. You never write a free: the counter triggers it, at the exact moment the last owner leaves. Vol 1's determinism is preserved — only, 'the owner' has become 'the last of the owners'.

Rc vs Arc : le prix de l'atomicitéRc vs Arc: the price of atomicity
use std::rc::Rc;
use std::sync::Arc;

let local = Rc::new(donnee.clone());  // un seul thread — compteur ordinaire, rapide
std::thread::spawn(move || { local });         // ✕ Rc n'est pas Send : ne compile pas

let partage = Arc::new(donnee);        // plusieurs threads — compteur ATOMIQUE
std::thread::spawn({ let p = Arc::clone(&partage); move || { /* p */ } });  // ✓
Atomique n'est pas gratuit.Atomic isn't free.

Arc fait la même chose que Rc, mais ses incréments/décréments sont des opérations atomiques — sûres entre threads, et un peu plus coûteuses qu'un simple += sur un entier. C'est pourquoi Rust ne te donne pas Arc par défaut : tu ne paies l'atomicité que si tu en as besoin. Un thread unique ? Rc. Plusieurs threads ? Arc. Le compilateur tranche pour toi : Rc n'est pas Send, donc le franchissement d'une frontière de thread ne compile qu'avec Arc. La sûreté concurrentielle est, là encore, vérifiée à la compilation — mais l'outil, lui, coûte à l'exécution.Arc does the same as Rc, but its increments/decrements are atomic operations — safe across threads, and a bit costlier than a plain += on an integer. That's why Rust doesn't give you Arc by default: you pay for atomicity only if you need it. A single thread? Rc. Several threads? Arc. The compiler decides for you: Rc isn't Send, so crossing a thread boundary only compiles with Arc. Concurrent safety is, again, checked at compile time — but the tool itself costs at runtime.

La donnée meurt non au premier drop, mais au dernier : le compteur sait qui éteint la lumière.The data dies not at the first drop, but at the last: the counter knows who turns off the light.
№ 04Le piègeThe trap4 min

Deux Rc qui se tiennent l'un l'autre : le compteur ne tombe jamais à zéro.Two Rcs holding each other: the count never falls to zero.

Voici la faille que Rust, exceptionnellement, ne ferme pas. Le comptage de références libère la donnée quand le compteur atteint zéro — mais que se passe-t-il si deux Rc se pointent mutuellement ? A maintient B en vie, B maintient A en vie. Quand toutes les variables externes disparaissent, chacun a encore un compteur à 1, à cause de l'autre. Le compteur n'atteint jamais zéro, le destructeur ne s'exécute jamais, et la mémoire fuit — silencieusement, pour toute la durée du programme. C'est la limite du modèle : la sûreté mémoire de Rust garantit l'absence d'accès invalides, pas l'absence de fuites. Un cycle de Rc est parfaitement « sûr » au sens du compilateur, et pourtant il perd de la mémoire. La sortie a un nom : Weak.Here's the gap that Rust, exceptionally, does not close. Reference counting frees the data when the count reaches zero — but what happens if two Rcs point at each other? A keeps B alive, B keeps A alive. When all external variables vanish, each still has a count of 1, because of the other. The count never reaches zero, the destructor never runs, and memory leaks — silently, for the program's whole life. It's the model's limit: Rust's memory safety guarantees no invalid accesses, not the absence of leaks. A cycle of Rcs is perfectly 'safe' in the compiler's sense, yet it loses memory. The exit has a name: Weak.

Le contre-exemple — un cycle de Rc forts, le compteur bloqué à 1The counter-example — a cycle of strong Rcs, the count stuck at 1
// Un parent qui tient ses enfants, et des enfants qui tiennent leur parent — en Rc
struct Noeud {
    parent: RefCell<Option<Rc<Noeud>>>,    // enfant → parent : Rc (fort)
    enfants: RefCell<Vec<Rc<Noeud>>>,      // parent → enfant : Rc (fort)
}

let parent = Rc::new(Noeud { /* ... */ });
let enfant = Rc::new(Noeud { /* ... */ });
parent.enfants.borrow_mut().push(Rc::clone(&enfant));   // parent tient enfant
*enfant.parent.borrow_mut() = Some(Rc::clone(&parent)); // enfant tient parent

// à la fin de portée : strong(parent) = 1, strong(enfant) = 1. JAMAIS 0.
// chacun maintient l'autre en vie → la mémoire FUIT. Rust ne l'empêche pas.
parent
strong = 1
Rc ──→possèdeowns←── Rcpossède aussi ✕also owns ✕
enfant
strong = 1
✕ jamais 0 → fuite✕ never 0 → leak

Les deux arêtes sont des Rc forts : parent possède enfant, enfant possède parent. Quand les variables locales sortent de portée, il reste une référence forte de chaque côté — celle que l'autre détient. Les deux compteurs stagnent à 1, aucun n'atteint zéro, et les deux nœuds vivent éternellement sans que personne ne puisse plus les atteindre. C'est une fuite mémoire authentique, dans un langage réputé pour ne pas en avoir.Both edges are strong Rcs: parent owns enfant, enfant owns parent. When the local variables go out of scope, one strong reference remains on each side — the one the other holds. Both counters stall at 1, neither reaches zero, and both nodes live forever while no one can reach them anymore. It's a genuine memory leak, in a language renowned for having none.

Le bon réflexe — l'arête retour en Weak, qui ne possède pasThe right reflex — the back-edge as Weak, which doesn't own
// L'arête « retour » devient FAIBLE : Weak ne compte pas dans le strong
struct Noeud {
    parent: RefCell<Weak<Noeud>>,          // enfant → parent : Weak (ne possède pas)
    enfants: RefCell<Vec<Rc<Noeud>>>,      // parent → enfant : Rc (possède)
}

*enfant.parent.borrow_mut() = Rc::downgrade(&parent);  // downgrade : Rc → Weak

// strong(parent) ne compte plus l'enfant. quand parent sort de portée,
// son strong tombe à 0 → libéré → puis l'enfant. plus de cycle, plus de fuite.
let p: Option<Rc<Noeud>> = enfant.parent.borrow().upgrade();  // Weak → Option<Rc>
parent
strong = 0 ✓
Rc ──→possèdeowns←·· Weakne possède pasdoesn't own
enfant
strong = 1
✓ cycle brisé✓ cycle broken

Weak<T> est une référence qui n'est pas propriétaire : elle pointe la donnée sans compter dans le strong, donc sans l'empêcher de mourir. On garde les liens « possédants » (parent → enfants) en Rc, et les liens « retour » (enfant → parent) en Weak. Le cycle de propriété est brisé : le strong du parent peut atteindre zéro. Pour lire un Weak, on l'upgrade() — qui rend un Option<Rc>, None si la cible a déjà été libérée. L'absence du Vol 3 ressurgit ici : un Weak dit honnêtement « cette cible existe peut-être encore ».Weak<T> is a reference that does not own: it points at the data without counting in the strong, so without preventing its death. You keep the 'owning' links (parent → children) as Rc, and the 'back' links (child → parent) as Weak. The ownership cycle is broken: the parent's strong can reach zero. To read a Weak, you upgrade() it — yielding an Option<Rc>, None if the target was already freed. Vol 3's absence resurfaces here: a Weak honestly says 'this target may still exist'.

La règle de décisionThe decision rule

« Ce lien doit-il maintenir la cible en vie ? » Oui → Rc (il possède). Non, il ne fait que la désigner tant qu'elle existe → Weak. Dans toute structure où des liens reviennent en arrière — parent/enfant, observateur/sujet, graphe — un sens porte la possession (Rc), l'autre la simple référence (Weak). Mélanger les deux sens en Rc est la recette du cycle.'Must this link keep the target alive?' Yes → Rc (it owns). No, it only designates it while it exists → Weak. In any structure where links point back — parent/child, observer/subject, graph — one direction carries ownership (Rc), the other a mere reference (Weak). Making both directions Rc is the recipe for a cycle.

La fuite est « sûre ».The leak is 'safe'.

C'est contre-intuitif : une fuite mémoire ne viole aucune garantie de sûreté. Aucune donnée n'est lue après libération, aucune course n'a lieu — la mémoire est juste retenue pour toujours. Rust promet l'absence de comportement indéfini, pas l'absence de gaspillage. Les cycles de Rc, comme Box::leak du Vol 2, sont des fuites parfaitement légales. C'est pourquoi le compilateur ne les attrape pas : à toi de concevoir tes liens pour qu'aucun cycle de Rc forts n'existe.It's counter-intuitive: a memory leak violates no safety guarantee. No data is read after free, no race occurs — the memory is just held forever. Rust promises no undefined behavior, not no waste. Rc cycles, like Vol 2's Box::leak, are perfectly legal leaks. That's why the compiler doesn't catch them: it's on you to design your links so no cycle of strong Rcs exists.

Un cycle de Rc forts est sûr mais immortel : la sortie est de rendre faible le lien qui n'a pas à posséder.A cycle of strong Rcs is safe but immortal: the exit is to weaken the link that needn't own.
№ 05Bilan & éditoWrap-up & editorial2 min

La possession partagée, comptée — et la fuite qu'elle rend, pour une fois, possible.Shared ownership, counted — and the leak it makes, for once, possible.

Ce numéro a ouvert l'arbre à plusieurs propriétaires. Rc tient un compteur de propriétaires d'une même donnée ; chaque clone l'incrémente, chaque destruction le décrémente, et la valeur meurt quand il tombe à zéro — déterministe, sans GC. Arc fait de même entre threads, au prix d'un compteur atomique. Mais ce pouvoir a un revers que Rust ne ferme pas : deux Rc qui se tiennent l'un l'autre forment un cycle dont le compteur n'atteint jamais zéro — une fuite, sûre mais réelle. Weak est la sortie : une référence qui désigne sans posséder, à brancher sur le lien retour. Le premier vrai prix de la sortie de l'arbre : un coût à l'exécution, et une responsabilité de conception.This issue opened the tree to multiple owners. Rc keeps a count of one piece of data's owners; each clone increments it, each destruction decrements it, and the value dies when it hits zero — deterministic, no GC. Arc does the same across threads, at the price of an atomic counter. But this power has a flip side Rust doesn't close: two Rcs holding each other form a cycle whose count never reaches zero — a leak, safe but real. Weak is the exit: a reference that designates without owning, to wire onto the back-link. The first real price of leaving the tree: a runtime cost, and a design responsibility.

L'outilThe toolCe qu'il installeWhat it sets upLe réflexeThe reflex
Rc<T>Plusieurs propriétaires, un compteur, libération à zéro.Several owners, a counter, freed at zero.Aucun propriétaire unique ne se dégage → Rc (un thread).No single owner emerges → Rc (one thread).
Arc<T>Le même, en sûr entre threads — compteur atomique.The same, thread-safe — atomic counter.Partage entre threads → Arc ; sinon Rc, moins cher.Cross-thread sharing → Arc; else Rc, cheaper.
Weak<T>Une référence qui désigne sans posséder ; upgrade → Option.A reference that designates without owning; upgrade → Option.Lien retour (parent, observateur) → Weak, pour briser le cycle.Back-link (parent, observer) → Weak, to break the cycle.
Cycle de RcRc cycleCompteur bloqué > 0 → fuite mémoire, sûre mais réelle.Counter stuck > 0 → memory leak, safe but real.Un seul sens possède ; l'autre est Weak. Jamais deux Rc.One direction owns; the other is Weak. Never two Rcs.
Rc partage la possession ; Weak partage la vue sans la vie. Confondre les deux, c'est fabriquer un cycle immortel.Rc shares ownership; Weak shares the view without the life. Confusing them builds an immortal cycle.
Mot de l'éditeurFrom the editor

On partage la donnée. Mais elle est figée — et si on doit la modifier ?We share the data. But it's frozen — and what if we must modify it?

Un détail est resté tu, et il est central. Rc ne donne qu'un accès partagé — donc en lecture seule. La règle du Vol 1 tient toujours : on peut avoir plusieurs &T, ou un seul &mut T, jamais les deux. Or plusieurs Rc sur une donnée, ce sont plusieurs accès partagés simultanés : muter à travers est, par construction, interdit. Tu peux partager un arbre, pas en changer un nœud. Pour la plupart des données partagées, c'est exactement ce qu'on veut. Mais parfois non — un cache qu'on met à jour, un nœud de graphe dont on change un lien, un compteur partagé.One detail stayed unspoken, and it's central. Rc gives only shared access — hence read-only. Vol 1's rule still holds: you may have several &T, or a single &mut T, never both. Yet several Rcs on one piece of data are several simultaneous shared accesses: mutating through them is, by construction, forbidden. You can share a tree, not change one of its nodes. For most shared data, that's exactly what you want. But sometimes not — a cache you update, a graph node whose link you change, a shared counter.

Vol 6 №3 : Cell et RefCell, la mutabilité intérieure. Comment muter une donnée derrière un accès partagé, sans violer la sûreté ? La réponse renverse la perspective : on déplace la vérification de l'emprunt de la compilation vers l'exécution. RefCell garde la règle « un seul écrivain à la fois », mais la vérifie au runtime — et panique si tu la enfreins, au lieu de refuser de compiler. C'est le dernier déplacement du volume : une garantie qu'on payait en rigueur statique, désormais payée en vérification dynamique. Souvent combiné à Rc — Rc<RefCell<T>> — pour obtenir enfin du partagé que l'on peut modifier.Vol 6 №3: Cell and RefCell, interior mutability. How do you mutate data behind shared access, without breaking safety? The answer flips the perspective: you move the borrow check from compile time to runtime. RefCell keeps the 'one writer at a time' rule, but checks it at runtime — and panics if you break it, instead of refusing to compile. It's the volume's final shift: a guarantee once paid in static rigor, now paid in dynamic checking. Often combined with Rc — Rc<RefCell<T>> — to finally get shared data you can modify.