Présentation du problème

Le besoin de dessiner plusieurs formes (cercle, carré, triangle) conduit à comparer deux stratégies de polymorphisme : le dispatch statique, où le type est connu à la compilation, et le dispatch dynamique, où le type est résolu à l’exécution. En C++, la première approche repose sur les fonctions virtuelles stockées dans une vtable, la seconde sur le CRTP. Rust propose dyn Trait pour le dispatch dynamique et la monomorphisation pour le dispatch statique.

Dispatch statique et monomorphisation

Le code suivant illustre le dispatch statique en Rust :

trait Draw { fn draw(&self) -> &str; }
struct Circle; struct Square;
impl Draw for Circle { fn draw(&self) -> &str { "Drawing a circle" } }
impl Draw for Square { fn draw(&self) -> &str { "Drawing a square" } }
fn draw_shape(shape: T) { println!("{}", shape.draw()); }
fn main() { let circle = Circle; let square = Square; draw_shape(circle); draw_shape(square); }

Le compilateur génère deux fonctions distinctes : draw_shape:: et draw_shape::. Aucun coût d’exécution supplémentaire n’est introduit, mais chaque spécialisation augmente la taille du binaire. Cette technique correspond au CRTP en C++, où le code est dupliqué pour chaque type dérivé.

Dispatch dynamique et structure des pointeurs larges

Le même exemple avec dyn Draw montre un changement de représentation :

fn draw_shape(shape: &dyn Draw) { println!("{}", shape.draw()); }
fn main() { let circle = Circle; let square = Square; draw_shape(&circle); draw_shape(&square); }

En Rust, un pointeur vers un trait objet occupe 16 octets, contre 8 octets pour un pointeur normal :

println!("&Circle size: {}", std::mem::size_of::<&Circle>());   // 8
println!("&dyn Draw size: {}", std::mem::size_of::<&dyn Draw>()); // 16

Les 8 octets supplémentaires contiennent l’adresse de la vtable, qui répertorie les implémentations concrètes des méthodes du trait. Le premier champ pointe vers l’objet réel, le second vers la vtable correspondante. Cette « fat pointer » permet au compilateur de choisir la bonne fonction d’affichage au moment de l’appel, reproduisant le mécanisme des vtables C++ mais encapsulé dans le type dyn Trait.

Types zéro‑size et gestion d’identité

Les structures sans champs, appelées Zero‑Sized Types (ZST), ont une taille de 0 octet :

println!("{}", std::mem::size_of::()); // 0
println!("{}", std::mem::size_of::()); // 0

Rust ne garantit pas d’adresse distincte pour chaque instance ZST. En mode debug, le compilateur attribue un emplacement de pile factice afin que le débogueur puisse afficher une adresse :

let a = Circle; let b = Circle;
println!("{:p}", &a as *const Circle);
println!("{:p}", &b as *const Circle);

Les deux adresses diffèrent d’un octet. En mode release, les adresses se confondent, révélant que le compilateur ne réserve aucun espace réel. L’unicité d’une valeur ZST est assurée par le système de possession et le borrow‑checker, non par l’adresse mémoire.