Les smart pointers Rust expliqués : Box, Rc, Arc et RefCell en 2026

Les smart pointers Rust Box, Rc, Arc et RefCell expliqués avec des exemples compilables 2026, un tableau de décision et les questions d'entretien courantes.

Diagramme de gestion mémoire des smart pointers Rust Box, Rc, Arc et RefCell

Les smart pointers de Rust sont les outils qui débloquent des schémas de possession que le borrow checker seul ne sait pas exprimer : allocation sur le tas, possession partagée et mutation derrière une référence partagée. Là où une simple référence (&T) ne fait qu'emprunter une valeur, un smart pointer possède ses données et ajoute un comportement supplémentaire par-dessus. Ce guide décortique les quatre types que tout développeur Rust rencontre en production et en entretien : Box, Rc, Arc et RefCell, avec des exemples compilables ciblant l'édition Rust 2024.

Le résumé en une ligne

Utiliser Box<T> pour une allocation sur le tas à propriétaire unique, Rc<T> pour la possession partagée sur un seul thread, Arc<T> pour la possession partagée entre threads, et RefCell<T> pour muter une valeur à travers une référence partagée. Les paires Rc<RefCell<T>> et Arc<Mutex<T>> couvrent l'état mutable partagé.

Ce que sont les smart pointers en Rust

Un smart pointer est une structure qui se comporte comme un pointeur mais transporte des métadonnées ou des capacités supplémentaires. La plupart implémentent le trait Deref, si bien que *pointer et les appels de méthode fonctionnent comme si le pointeur était une simple référence, ainsi que le trait Drop, si bien que le nettoyage s'exécute automatiquement quand la valeur sort de la portée. La bibliothèque standard fournit les quatre types couverts ici, et les comprendre repose sur une bonne maîtrise de la possession et de l'emprunt, qui déterminent qui libère chaque allocation et quand.

La distinction clé avec une référence tient à la possession. &T ne possède jamais les données qu'il pointe, il ne peut donc pas survivre à la valeur. Un smart pointer possède les données, contrôle leur durée de vie et les libère de façon déterministe. Le chapitre du Rust Book sur les smart pointers les traite comme une catégorie justement parce qu'ils partagent cette forme possession-plus-comportement.

Box<T> : allocation sur le tas pour les types récursifs et dimensionnés

Box<T> est le smart pointer le plus simple. Il stocke une valeur sur le tas et conserve un pointeur vers elle sur la pile, avec un propriétaire unique et aucun surcoût à l'exécution au-delà de l'allocation elle-même. Son rôle le plus courant est de donner une taille connue aux types récursifs : un type qui se contient lui-même directement serait infiniment grand, mais un Box n'est qu'un pointeur, sa taille est donc fixe quel que soit ce qu'il pointe.

L'exemple ci-dessous définit un arbre binaire. Chaque Node contient deux enfants, et sans indirection le compilateur ne peut pas calculer la taille de Tree. Envelopper chaque enfant dans un Box casse la récursion au niveau du type.

tree.rsrust
#[derive(Debug)]
enum Tree {
    Leaf(i32),
    // Box<Tree> puts each child on the heap, so Node has a fixed size (two pointers).
    Node(Box<Tree>, Box<Tree>),
}

fn sum(tree: &Tree) -> i32 {
    match tree {
        Tree::Leaf(value) => *value,                        // base case: return the leaf
        Tree::Node(left, right) => sum(left) + sum(right),  // recurse into both children
    }
}

fn main() {
    // Build (1) + ((2) + (3)) = 6
    let tree = Tree::Node(
        Box::new(Tree::Leaf(1)),
        Box::new(Tree::Node(
            Box::new(Tree::Leaf(2)),
            Box::new(Tree::Leaf(3)),
        )),
    );
    println!("sum = {}", sum(&tree)); // sum = 6
}

Box est également utile quand déplacer une grande valeur serait coûteux, ou quand une fonction renvoie un objet trait comme Box<dyn Error>. Dans tous les cas la règle est la même : un seul propriétaire, libéré automatiquement quand le Box est détruit.

Rc<T> : possession partagée en code monothread

Parfois une valeur a besoin de plusieurs propriétaires, dont aucun n'est manifestement le dernier à l'utiliser. Rc<T> (compté par référence) résout cela en tenant un compteur fort du nombre de propriétaires existants. Rc::clone incrémente ce compteur sans copier les données sous-jacentes, et chaque destruction le décrémente. Quand le compteur atteint zéro, la valeur est libérée.

Prenons un objet de configuration que plusieurs workers lisent. Chaque worker doit conserver la configuration aussi longtemps qu'il en a besoin, et l'allocation ne doit disparaître que lorsque le dernier worker a disparu.

shared_config.rsrust
use std::rc::Rc;

#[derive(Debug)]
struct Config {
    endpoint: String,
    timeout_ms: u32,
}

fn main() {
    // Rc::new moves Config onto the heap with a strong count of 1.
    let config = Rc::new(Config {
        endpoint: "https://api.example.com".to_string(),
        timeout_ms: 5000,
    });

    // Rc::clone only bumps the reference count; it does not deep-copy Config.
    let worker_a = Rc::clone(&config);
    let worker_b = Rc::clone(&config);

    // All three handles point to the same allocation.
    println!("endpoint: {}", worker_a.endpoint);
    println!("timeout:  {}", worker_b.timeout_ms);

    // strong_count reports how many owners currently hold the value.
    println!("owners: {}", Rc::strong_count(&config)); // owners: 3
}

Appeler Rc::clone explicitement (plutôt que config.clone()) est idiomatique : cela signale au lecteur que l'opération est peu coûteuse et ne touche qu'un compteur. Le piège, c'est que Rc<T> ne distribue que des références partagées et immuables. Il ne peut pas muter la valeur qu'il partage, et il n'est pas sûr de l'envoyer entre threads. La documentation de Rc détaille ces deux contraintes.

RefCell<T> et la mutabilité intérieure en Rust

Rust impose normalement qu'une valeur soit soit partagée via de nombreuses références immuables, soit mutée via exactement une référence mutable, et il le vérifie à la compilation. RefCell<T> fournit la mutabilité intérieure : il permet au code de muter une valeur à travers une référence partagée en déplaçant cette même vérification à l'exécution. borrow() renvoie un garde de lecture partagé ; borrow_mut() renvoie un garde d'écriture exclusif. Les règles sont identiques, mais les violations provoquent une panique au lieu d'échouer à la compilation.

Combiné à Rc, cela produit le schéma canonique monothread de partage mutable, Rc<RefCell<T>> : plusieurs propriétaires qui peuvent tous mettre à jour la même valeur.

counter.rsrust
use std::rc::Rc;
use std::cell::RefCell;

// Rc gives shared ownership; RefCell allows mutation through a shared reference.
type SharedCounter = Rc<RefCell<u32>>;

fn increment(counter: &SharedCounter) {
    // borrow_mut() hands out an exclusive reference, checked at runtime.
    *counter.borrow_mut() += 1;
}

fn main() {
    let counter: SharedCounter = Rc::new(RefCell::new(0));

    let handle_a = Rc::clone(&counter);
    let handle_b = Rc::clone(&counter);

    increment(&handle_a);
    increment(&handle_b);
    increment(&counter);

    // borrow() gives a shared read guard; the value is now 3.
    println!("count = {}", counter.borrow()); // count = 3
}
RefCell déplace la vérification d'emprunt à l'exécution

Appeler borrow_mut() alors qu'un autre garde borrow() ou borrow_mut() est encore vivant provoque une panique avec already borrowed: BorrowMutError. Les garanties de sûreté tiennent, mais un bug logique que le compilateur aurait détecté devient un plantage à l'exécution. Il faut garder les gardes de courte durée et éviter de conserver un emprunt à travers un appel de fonction susceptible de rentrer à nouveau dans le même RefCell.

La mutabilité intérieure est aussi l'endroit où se cachent les cycles de références. Deux valeurs Rc<RefCell<T>> qui se pointent l'une l'autre maintiennent mutuellement leur compteur fort au-dessus de zéro pour toujours, si bien qu'aucune n'est jamais libérée. La solution est Weak<T>, un handle non possédant qui n'affecte pas le compteur fort ; l'explication classique se trouve dans Learn Rust With Entirely Too Many Linked Lists.

Arc<T> : comptage de références thread-safe pour la concurrence

Arc<T> (compté par référence de façon atomique) est le pendant multithread de Rc<T>. L'API publique est presque identique, mais le compteur utilise des opérations atomiques, si bien que cloner et détruire un Arc depuis plusieurs threads à la fois reste correct. Cette atomicité explique pourquoi Arc n'est choisi que lorsque le partage franchit une frontière de thread : l'incrément atomique est mesurablement plus lent que l'incrément simple qu'utilise Rc.

Comme Arc<T> ne distribue toujours que des références immuables, muter entre threads nécessite une primitive de synchronisation. Mutex<T> est le partenaire habituel, donnant le schéma Arc<Mutex<T>> qui reflète Rc<RefCell<T>> pour le code concurrent. L'exemple lance quatre threads qui incrémentent chacun un total partagé un millier de fois.

parallel_sum.rsrust
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Arc is safe to share across threads; Mutex serializes access to the inner value.
    let total = Arc::new(Mutex::new(0u64));
    let mut handles = Vec::new();

    for _ in 0..4 {
        // Each thread gets its own Arc handle (an atomic count bump).
        let total = Arc::clone(&total);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                // lock() blocks until the mutex is free, then returns a guard.
                let mut value = total.lock().unwrap();
                *value += 1;
            }
        }));
    }

    for handle in handles {
        handle.join().unwrap(); // wait for every thread before reading
    }

    println!("total = {}", *total.lock().unwrap()); // total = 4000
}

Le compilateur impose la frontière à votre place : Rc<T> n'est pas Send, donc tenter d'en déplacer un dans thread::spawn échoue à la compilation, ce qui pousse vers Arc. Recourir à Arc à l'intérieur d'un seul thread ne fait que payer des atomiques jamais utilisées. La documentation de Arc couvre en détail les garanties d'ordonnancement mémoire, et Arc<Mutex<T>> sous-tend une grande partie de l'état partagé dans Rust asynchrone avec Tokio.

Prêt à réussir tes entretiens Rust ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Box, Rc, Arc ou RefCell : lequel choisir

Les quatre types se composent selon deux axes : combien de propriétaires possède une valeur, et si elle peut être mutée à travers un handle partagé. Le tableau résume les compromis.

| Type | Possession | Mutation via un handle partagé | Thread-safe | Surcoût | |------|-----------|----------------------------|-------------|----------| | Box<T> | Unique | Non | Oui, si T: Send | Allocation sur le tas uniquement | | Rc<T> | Partagée | Non | Non | Compteur non atomique | | Arc<T> | Partagée | Non | Oui | Compteur atomique | | RefCell<T> | Unique | Oui (vérifiée à l'exécution) | Non | Drapeau d'emprunt à l'exécution |

L'arbre de décision pratique est court. Besoin d'un seul propriétaire sur le tas : Box. Besoin de plusieurs propriétaires sur un seul thread : Rc. Besoin de plusieurs propriétaires entre threads : Arc. Besoin de muter une valeur partagée : envelopper le type interne dans RefCell (monothread) ou Mutex (multithread). Les recommandations d'Effective Rust sur les types référence et pointeur aboutissent au même empilement. Commencer par l'option la moins puissante et n'ajouter de la capacité que lorsque le compilateur l'exige.

Questions d'entretien sur les smart pointers Rust

Les smart pointers sont un sujet d'entretien fiable parce qu'ils testent si un candidat comprend la possession plutôt que la seule syntaxe. Les questions ci-dessous reviennent souvent, et d'autres sont rassemblées dans le module d'entretien sur les smart pointers Rust.

Quelle est la différence entre Rc et Arc ? Les deux fournissent la possession partagée par comptage de références. Rc utilise un compteur non atomique et est confiné à un seul thread ; Arc utilise des opérations atomiques et peut être partagé entre threads à un léger coût à l'exécution. Le compilateur impose la séparation : Rc n'est ni Send ni Sync, il ne peut donc pas franchir une frontière de thread.

Pourquoi combiner Rc avec RefCell ? Rc accorde la possession partagée mais seulement un accès immuable. RefCell ajoute la mutabilité intérieure, permettant aux propriétaires de muter la valeur à travers une référence partagée avec des vérifications d'emprunt à l'exécution. Ensemble, Rc<RefCell<T>> est la brique idiomatique de partage mutable monothread.

Comment le comptage de références peut-il provoquer une fuite mémoire en Rust ? Deux valeurs Rc (ou Arc) qui se référencent mutuellement forment un cycle dont les compteurs forts n'atteignent jamais zéro, si bien que l'allocation n'est jamais libérée. Casser le cycle avec Weak<T>, qui détient une référence non possédante, rétablit un nettoyage correct.

Quand Box est-il préférable à Rc ? Chaque fois qu'un seul propriétaire suffit. Box n'a aucun surcoût de comptage de références, c'est donc le choix par défaut pour l'allocation sur le tas, les types récursifs et les objets trait. Ne recourir à Rc que lorsqu'une véritable possession partagée est nécessaire.

Conclusion

Les smart pointers de Rust transforment la possession, de contrainte en un ensemble de briques composables. Le choix entre eux découle directement de la forme du problème :

  • Recourir à Box<T> quand une valeur a besoin d'un unique propriétaire sur le tas, qu'un type récursif a besoin d'une taille fixe, ou qu'une fonction renvoie un objet trait.
  • Utiliser Rc<T> pour plusieurs propriétaires sur un seul thread, et basculer vers Arc<T> dès que la possession franchit une frontière de thread.
  • Ajouter RefCell<T> pour la mutabilité intérieure en code monothread, et Mutex<T> pour l'équivalent concurrent, en gardant les gardes d'emprunt et de verrou de courte durée.
  • Les combiner délibérément : Rc<RefCell<T>> pour l'état mutable partagé sur un seul thread, Arc<Mutex<T>> entre threads.
  • Surveiller les cycles de références entre pointeurs comptés et les casser avec Weak<T> pour éviter les fuites.
  • Choisir par défaut le type le moins puissant et laisser le compilateur indiquer quand plus de capacité est nécessaire.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#rust
#smart-pointers
#memory-management
#interview
#rust-2024-edition

Partager

Articles similaires