Cours de programmation séquentielle

Pointeurs intelligents

Orestis Malaspinas

Pointeurs

Généralités

  • Un pointeur est une variable qui contient une adresse mémoire.
  • Cette adresse pointe vers des données.
Illustration: tableau.
Illustration: tableau.
  • Question: Quel type de pointeur avons-nous déjà rencontré?
    • Réponse: La référence.

Smart pointers

  • Un pointeur intelligent est un type abstrait qui rajoute des fonctionnalités au poiteur standard.
    • Management automatique de la mémoire.
    • Vérification de limites.
  • En particulier, ils permettent la désallocation de la mémoire de manière automatique:
    • On peut avoir plusieurs pointeurs sur un espace mémoire.
    • Quand le dernier pointeurs est détruit, l’espace mémoire est désalloué.
    • Permet d’empêcher les fuites mémoires.
  • Il existe différents types:
    • Un poiteur unique est le propriétaire de ses données (quand il est détruit les données aussi).
    • On compte le nombre de références sur des données, quand ce nombre tombe à zéro on détruit tout.

En Rust

  • Exemples:
    • Vec<T>, String: ces types possèdent de la mémoire et la manipule eux-mêmes.
  • Les pointeurs intelligent doivent implémenter deux traits:
    • Deref: comment on déréférence le pointeur.
    • Drop: comment on détruit le pointeur.
  • Les trois cas les plus typiques (il y en a d’autres):
    • Box<T> pointeur unique qui alloue des données sur la pile.
    • Rc<T> “reference counted” type, qui permet de partager la propriété.
    • Ref<T> et RefMut<T> qui permet d’imposer les règles de propriété à l’exécution plutôt qu’à la compilation.

Le type Box<T>

Généralités

  • Utile pour stocker des données sur le tas.
  • La seule chose stockée sur la pile est le pointeur sur les données du tas.
  • Cas typiques d’utilisation:
    • Quand la taille d’un type est inconnu à la compilation.
    • Quand on veut transférer la propriété de grandes quantité de données mais s’assurer que les données ne seront pas copiées (juste le pointeur).

Utilisation


fn main() 
{
    let num = Box::new(10);
    println!("num = {}", num);  // déréférenciation automatique
    println!("num = {}", *num); // déréférenciation explicite

    let x = 10;
    // seule la déréférenciation explicite marche
    println!("Is {} equal to {}?. Answer: {}", num, x, *num == x); 

    let y = &x;
    // La seule différence est qu'ici nous avons une référence 
    println!("Is {} equal to {}?. Answer: {}", y, x, *y == x);
} 
  • Dans l’appel de fonctions/méthodes la déréférenciation se fait automatiquement.
  • Sinon elle doit être explicite.

Car pratique: la liste chaînée

  • Rust doit connaître la taille d’un type à la compilation.
  • Ici nous avons à faire à un type récursif: sa taille est potentiellement infinie.

enum List {
    Elem(i32, List),
    Nil,
}

use List::{Elem, Nil};

fn main() 
{
    let list = Elem(1, Elem(2, Elem(3, Nil)));
} 

Car pratique: la liste chaînée

  • Avec un Box<T> sa taille est connue.

#[derive(Debug)]
enum List {
    Elem(i32, Box<List>),
    Nil,
}

use List::{Elem, Nil};

fn main() 
{
    let list = Elem(1, Box::new(Elem(2, Box::new(Elem(3, Box::new(Nil))))));
    println!("{:?}", list);
}