384: Trait Objects and `dyn Trait`
Tutorial Video
Text description (accessibility)
This video demonstrates the "384: Trait Objects and `dyn Trait`" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Static dispatch (generics with monomorphization) produces the fastest code but requires knowing all concrete types at compile time. Key difference from OCaml: 1. **Fat pointer size**: Rust's `Box<dyn Trait>` is 16 bytes (two pointers); OCaml objects carry a tag word and method table pointer — similar overhead.
Tutorial
The Problem
Static dispatch (generics with monomorphization) produces the fastest code but requires knowing all concrete types at compile time. Sometimes the set of types is open and determined at runtime — a plugin system, a heterogeneous collection, or a callback registered by user code. Dynamic dispatch via dyn Trait solves this: values are stored as fat pointers (data pointer + vtable pointer), enabling runtime polymorphism at the cost of an indirect function call per virtual method.
dyn Trait is used in GUI frameworks (event handlers), plugin architectures, Box<dyn Error> in error handling, the std::io::Read/Write traits, and anywhere a collection needs to hold mixed types.
🎯 Learning Outcomes
impl Trait / generics) and dynamic dispatch (dyn Trait)Box<dyn Animal> enables heterogeneous collections in RustdynCode Example
#![allow(clippy::all)]
//! dyn Trait and Fat Pointers
pub trait Animal {
fn speak(&self) -> String;
fn name(&self) -> &str;
}
pub struct Dog {
pub name: String,
}
pub struct Cat {
pub name: String,
}
impl Animal for Dog {
fn speak(&self) -> String {
"Woof!".to_string()
}
fn name(&self) -> &str {
&self.name
}
}
impl Animal for Cat {
fn speak(&self) -> String {
"Meow!".to_string()
}
fn name(&self) -> &str {
&self.name
}
}
pub fn make_noise(animals: &[Box<dyn Animal>]) -> Vec<String> {
animals
.iter()
.map(|a| format!("{}: {}", a.name(), a.speak()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dog() {
let d = Dog {
name: "Rex".to_string(),
};
assert_eq!(d.speak(), "Woof!");
}
#[test]
fn test_cat() {
let c = Cat {
name: "Whiskers".to_string(),
};
assert_eq!(c.speak(), "Meow!");
}
#[test]
fn test_heterogeneous_vec() {
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog {
name: "Rex".to_string(),
}),
Box::new(Cat {
name: "Whiskers".to_string(),
}),
];
let noises = make_noise(&animals);
assert!(noises[0].contains("Woof"));
assert!(noises[1].contains("Meow"));
}
#[test]
fn test_trait_object_size() {
// Fat pointer: 2 words (ptr + vtable)
assert_eq!(
std::mem::size_of::<&dyn Animal>(),
2 * std::mem::size_of::<usize>()
);
}
}Key Differences
Box<dyn Trait> is 16 bytes (two pointers); OCaml objects carry a tag word and method table pointer — similar overhead.Box<dyn Animal> is never null; OCaml objects can be the Obj.magic null value in unsafe code.dyn to be object-safe (no generic methods, no Self in return positions); OCaml's object methods have no equivalent restriction.dyn; OCaml uses algebraic types for the same purpose with exhaustiveness checking.OCaml Approach
OCaml achieves the same effect through its object system. Classes define virtual methods, and any Animal object holds a method table pointer. Alternatively, OCaml uses first-class modules: (module Animal : ANIMAL). The most idiomatic approach uses algebraic types with pattern matching, avoiding dynamic dispatch entirely when the type set is closed. For open type sets, OCaml's extensible variant types or object methods provide runtime dispatch.
Full Source
#![allow(clippy::all)]
//! dyn Trait and Fat Pointers
pub trait Animal {
fn speak(&self) -> String;
fn name(&self) -> &str;
}
pub struct Dog {
pub name: String,
}
pub struct Cat {
pub name: String,
}
impl Animal for Dog {
fn speak(&self) -> String {
"Woof!".to_string()
}
fn name(&self) -> &str {
&self.name
}
}
impl Animal for Cat {
fn speak(&self) -> String {
"Meow!".to_string()
}
fn name(&self) -> &str {
&self.name
}
}
pub fn make_noise(animals: &[Box<dyn Animal>]) -> Vec<String> {
animals
.iter()
.map(|a| format!("{}: {}", a.name(), a.speak()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dog() {
let d = Dog {
name: "Rex".to_string(),
};
assert_eq!(d.speak(), "Woof!");
}
#[test]
fn test_cat() {
let c = Cat {
name: "Whiskers".to_string(),
};
assert_eq!(c.speak(), "Meow!");
}
#[test]
fn test_heterogeneous_vec() {
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog {
name: "Rex".to_string(),
}),
Box::new(Cat {
name: "Whiskers".to_string(),
}),
];
let noises = make_noise(&animals);
assert!(noises[0].contains("Woof"));
assert!(noises[1].contains("Meow"));
}
#[test]
fn test_trait_object_size() {
// Fat pointer: 2 words (ptr + vtable)
assert_eq!(
std::mem::size_of::<&dyn Animal>(),
2 * std::mem::size_of::<usize>()
);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dog() {
let d = Dog {
name: "Rex".to_string(),
};
assert_eq!(d.speak(), "Woof!");
}
#[test]
fn test_cat() {
let c = Cat {
name: "Whiskers".to_string(),
};
assert_eq!(c.speak(), "Meow!");
}
#[test]
fn test_heterogeneous_vec() {
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog {
name: "Rex".to_string(),
}),
Box::new(Cat {
name: "Whiskers".to_string(),
}),
];
let noises = make_noise(&animals);
assert!(noises[0].contains("Woof"));
assert!(noises[1].contains("Meow"));
}
#[test]
fn test_trait_object_size() {
// Fat pointer: 2 words (ptr + vtable)
assert_eq!(
std::mem::size_of::<&dyn Animal>(),
2 * std::mem::size_of::<usize>()
);
}
}
Deep Comparison
OCaml vs Rust: Trait Objects
Exercises
PluginRegistry that stores Box<dyn Plugin> values keyed by name string. Implement register, run_all, and run_by_name methods. Show how new plugins can be added at runtime without changing the registry code.Vec<Box<dyn Trait>> with a generic function over a concrete type, measuring the overhead of vtable dispatch for a tight loop of 1 million calls.Box<dyn Iterator<Item = i32>> that wraps different iterator types, showing how dyn enables storing iterators of different concrete types in the same variable.