Ce qui se cache derrière le pointeur « gras » ? Analyse du fat pointer en Rust
\n\nImaginez que vous allez à la bibliothèque et demandez un livre. Le bibliothécaire vous donne un petit papier avec le numéro de la rangée et de l'étagère — c'est un pointeur ordinaire. Mais que faire si vous ne connaissez pas la taille du livre ? Ou si c'est un livre en trois volumes ? Vous aurez besoin d'un deuxième papier : « Tome 1 sur 3 » ou « Longueur : 450 pages ».
\n\nEn Rust, un tel pointeur « double » est appelé fat pointer (pointeur gras). Ce n'est pas de la magie, mais simplement une paire de nombres : l'adresse des données et leurs méta-informations (le plus souvent la longueur ou un pointeur vers une table virtuelle de méthodes). Voyons à quoi cela sert et comment cela fonctionne, avec des exemples de code concrets.
\n\nLes pointeurs ordinaires : fins et rapides
\n\nCommençons par les bases. En Rust, il existe des pointeurs simples — &T (référence) ou *const T (pointeur brut). Ils occupent exactement 8 octets (sur un système 64 bits) et ne stockent que l'adresse mémoire.
fn main() {\n let x: i32 = 42;\n let ptr: &i32 = &x; // pointeur fin — 8 octets\n println!("Taille de la référence : {} octets", std::mem::size_of_val(&ptr));\n}\n\n\nSortie : Taille de la référence : 8 octets. Tout est prévisible : le compilateur sait que i32 fait 4 octets, donc aucune information supplémentaire n'est nécessaire. Mais que faire si la taille des données est inconnue à l'avance ?
Premier fat pointer : les slices (&[T])
\n\nUne slice est une « fenêtre » sur un tableau ou un vecteur. Mais pour l'utiliser, il faut connaître non seulement le début, mais aussi le nombre d'éléments. Rust stocke cela comme une paire (ptr, len) — c'est ce qu'on appelle un fat pointer.
fn main() {\n let arr = [1, 2, 3, 4, 5];\n let slice: &[i32] = &arr[1..4]; // slice de 3 éléments\n\n println!("Taille de la slice : {} octets", std::mem::size_of_val(&slice));\n println!("Éléments : {:?}", slice);\n}\n\n\nSortie : Taille de la slice : 16 octets (8 octets pour le pointeur + 8 octets pour la longueur). Si nous ne transmettions que l'adresse, nous ne saurions pas où nous arrêter. C'est pourquoi la slice est un exemple classique de fat pointer.
D'ailleurs, la chaîne &str fonctionne exactement de la même manière : c'est un fat pointer (*const u8, usize). C'est pourquoi &str occupe 16 octets, tandis que &String n'en occupe que 8 (car String stocke lui-même la longueur en interne).
Deuxième fat pointer : les objets trait (&dyn Trait)
\n\nEn Rust, il existe une dispatch dynamique via les objets trait. Lorsque vous écrivez &dyn MyTrait, le compilateur ne sait pas quel type concret sera passé. Il a besoin d'une table virtuelle de méthodes (vtable) pour appeler les bonnes fonctions.
trait Animal {\n fn speak(&self);\n}\n\nstruct Dog;\nstruct Cat;\n\nimpl Animal for Dog {\n fn speak(&self) { println!("Ouaf !"); }\n}\n\nimpl Animal for Cat {\n fn speak(&self) { println!("Miaou !"); }\n}\n\nfn main() {\n let dog = Dog;\n let cat = Cat;\n\n let animals: [&dyn Animal; 2] = [&dog, &cat];\n\n println!("Taille de &dyn Animal : {} octets", std::mem::size_of::<&dyn Animal>());\n\n for animal in &animals {\n animal.speak();\n }\n}\n\n\nSortie : Taille de &dyn Animal : 16 octets. Ces 16 octets sont à nouveau un fat pointer : les 8 premiers octets sont l'adresse des données (Dog ou Cat), les 8 seconds sont l'adresse de la vtable (table des méthodes).
Quel est l'impact du fat pointer sur les performances ?
\n\nLes fat pointers sont un compromis. D'un côté, ils transportent plus d'informations, ce qui permet de travailler avec des types dynamiques. De l'autre côté :
\n\n- \n
- Mémoire : un fat pointer occupe 16 octets au lieu de 8. Ce n'est pas critique, mais dans de grandes collections, cela peut avoir un impact. \n
- Vitesse : l'accès via la vtable (pour les objets trait) est légèrement plus lent qu'un appel de méthode direct (monomorphisation). La diffé