ExamplesBy LevelBy TopicLearning Paths
384 Intermediate

384: Trait Objects and `dyn Trait`

Functional Programming

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

  • • Understand the difference between static dispatch (impl Trait / generics) and dynamic dispatch (dyn Trait)
  • • Learn what a vtable is and how fat pointers enable runtime polymorphism
  • • See how Box<dyn Animal> enables heterogeneous collections in Rust
  • • Understand the object safety rules that restrict which traits can be used with dyn
  • • Learn the performance trade-off: monomorphization vs. vtable indirection
  • Code 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

  • 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.
  • Null safety: Rust's Box<dyn Animal> is never null; OCaml objects can be the Obj.magic null value in unsafe code.
  • Object safety: Rust requires traits used with dyn to be object-safe (no generic methods, no Self in return positions); OCaml's object methods have no equivalent restriction.
  • Alternatives: Rust offers enum dispatch as a closed-set alternative to 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>()
            );
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Plugin registry: Build a 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.
  • Type erasure benchmark: Write a benchmark comparing 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.
  • dyn Trait with state: Implement a Box<dyn Iterator<Item = i32>> that wraps different iterator types, showing how dyn enables storing iterators of different concrete types in the same variable.
  • Open Source Repos