ExamplesBy LevelBy TopicLearning Paths
076 Intermediate

076 — Trait Objects (Dynamic Dispatch)

Functional Programming

Tutorial

The Problem

Trait objects (dyn Trait) enable runtime polymorphism in Rust — the ability to work with different types through a common interface without knowing the concrete type at compile time. They are Rust's answer to OOP inheritance and interface polymorphism: Vec<Box<dyn Shape>> can hold circles, rectangles, and triangles in one collection.

Dynamic dispatch via dyn Trait is used in plugin systems, event handlers, GUI widget trees, game entity systems, and any architecture requiring heterogeneous collections. The trade-off: dynamic dispatch has a small vtable lookup overhead but enables flexibility that static generics cannot provide.

🎯 Learning Outcomes

  • • Define traits with methods and implement them for multiple types
  • • Use &dyn Trait and Box<dyn Trait> for dynamic dispatch
  • • Understand the vtable: a pointer to the trait implementation for the concrete type
  • • Compare dyn Trait (runtime polymorphism) vs generics <T: Trait> (compile-time monomorphization)
  • • Recognize that dyn Trait cannot be used with non-object-safe traits
  • Code Example

    #![allow(clippy::all)]
    // 076: Trait Objects — dynamic dispatch with dyn Trait
    
    use std::f64::consts::PI;
    
    // Approach 1: Define trait and implementations
    trait Shape {
        fn area(&self) -> f64;
        fn name(&self) -> &str;
    }
    
    struct Circle {
        radius: f64,
    }
    struct Rectangle {
        width: f64,
        height: f64,
    }
    
    impl Shape for Circle {
        fn area(&self) -> f64 {
            PI * self.radius * self.radius
        }
        fn name(&self) -> &str {
            "circle"
        }
    }
    
    impl Shape for Rectangle {
        fn area(&self) -> f64 {
            self.width * self.height
        }
        fn name(&self) -> &str {
            "rectangle"
        }
    }
    
    // Approach 2: Using dyn Trait for polymorphism
    fn describe(shape: &dyn Shape) -> String {
        format!("{} with area {:.2}", shape.name(), shape.area())
    }
    
    fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
        shapes.iter().map(|s| s.area()).sum()
    }
    
    // Approach 3: Returning trait objects
    fn make_shape(kind: &str) -> Box<dyn Shape> {
        match kind {
            "circle" => Box::new(Circle { radius: 5.0 }),
            "rectangle" => Box::new(Rectangle {
                width: 3.0,
                height: 4.0,
            }),
            _ => Box::new(Circle { radius: 1.0 }),
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_circle() {
            let c = Circle { radius: 5.0 };
            assert!((c.area() - 78.54).abs() < 0.01);
            assert_eq!(c.name(), "circle");
        }
    
        #[test]
        fn test_rectangle() {
            let r = Rectangle {
                width: 3.0,
                height: 4.0,
            };
            assert_eq!(r.area(), 12.0);
        }
    
        #[test]
        fn test_dyn_dispatch() {
            let shapes: Vec<Box<dyn Shape>> = vec![
                Box::new(Circle { radius: 5.0 }),
                Box::new(Rectangle {
                    width: 3.0,
                    height: 4.0,
                }),
            ];
            assert!((total_area(&shapes) - 90.54).abs() < 0.01);
        }
    
        #[test]
        fn test_make_shape() {
            let s = make_shape("circle");
            assert_eq!(s.name(), "circle");
        }
    }

    Key Differences

  • **dyn Trait vs records**: Rust's vtable is automatic — define the trait, implement it, use dyn Trait. OCaml requires manually building record-of-functions vtables, or using the OO subset (#name).
  • Object safety: Rust's dyn Trait requires "object safety": no methods with Self return type, no generic methods. OCaml's record-of-functions approach has no such restriction.
  • **Box for ownership**: Box<dyn Trait> owns the object. &dyn Trait borrows it. OCaml's record-of-functions is always heap-allocated (via GC) — no explicit boxing.
  • Monomorphization vs vtable: fn area<T: Shape>(s: &T) monomorphizes (separate code per type, fast). fn area(s: &dyn Shape) uses vtable (one code path, flexible). OCaml's records are always vtable-style.
  • OCaml Approach

    OCaml uses record-of-functions as its idiomatic "dynamic dispatch" (manually built vtable):

    type shape = {
      area : unit -> float;
      name : unit -> string;
    }
    
    let circle r = {
      area = (fun () -> Float.pi *. r *. r);
      name = (fun () -> "circle");
    }
    
    let rectangle w h = {
      area = (fun () -> w *. h);
      name = (fun () -> "rectangle");
    }
    
    let describe s = Printf.printf "%s: %.2f\n" (s.name ()) (s.area ())
    

    OCaml's OO subset (#method) provides an alternative with structural subtyping. The record-of-functions approach mirrors Rust's vtable more directly.

    Full Source

    #![allow(clippy::all)]
    // 076: Trait Objects — dynamic dispatch with dyn Trait
    
    use std::f64::consts::PI;
    
    // Approach 1: Define trait and implementations
    trait Shape {
        fn area(&self) -> f64;
        fn name(&self) -> &str;
    }
    
    struct Circle {
        radius: f64,
    }
    struct Rectangle {
        width: f64,
        height: f64,
    }
    
    impl Shape for Circle {
        fn area(&self) -> f64 {
            PI * self.radius * self.radius
        }
        fn name(&self) -> &str {
            "circle"
        }
    }
    
    impl Shape for Rectangle {
        fn area(&self) -> f64 {
            self.width * self.height
        }
        fn name(&self) -> &str {
            "rectangle"
        }
    }
    
    // Approach 2: Using dyn Trait for polymorphism
    fn describe(shape: &dyn Shape) -> String {
        format!("{} with area {:.2}", shape.name(), shape.area())
    }
    
    fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
        shapes.iter().map(|s| s.area()).sum()
    }
    
    // Approach 3: Returning trait objects
    fn make_shape(kind: &str) -> Box<dyn Shape> {
        match kind {
            "circle" => Box::new(Circle { radius: 5.0 }),
            "rectangle" => Box::new(Rectangle {
                width: 3.0,
                height: 4.0,
            }),
            _ => Box::new(Circle { radius: 1.0 }),
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_circle() {
            let c = Circle { radius: 5.0 };
            assert!((c.area() - 78.54).abs() < 0.01);
            assert_eq!(c.name(), "circle");
        }
    
        #[test]
        fn test_rectangle() {
            let r = Rectangle {
                width: 3.0,
                height: 4.0,
            };
            assert_eq!(r.area(), 12.0);
        }
    
        #[test]
        fn test_dyn_dispatch() {
            let shapes: Vec<Box<dyn Shape>> = vec![
                Box::new(Circle { radius: 5.0 }),
                Box::new(Rectangle {
                    width: 3.0,
                    height: 4.0,
                }),
            ];
            assert!((total_area(&shapes) - 90.54).abs() < 0.01);
        }
    
        #[test]
        fn test_make_shape() {
            let s = make_shape("circle");
            assert_eq!(s.name(), "circle");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_circle() {
            let c = Circle { radius: 5.0 };
            assert!((c.area() - 78.54).abs() < 0.01);
            assert_eq!(c.name(), "circle");
        }
    
        #[test]
        fn test_rectangle() {
            let r = Rectangle {
                width: 3.0,
                height: 4.0,
            };
            assert_eq!(r.area(), 12.0);
        }
    
        #[test]
        fn test_dyn_dispatch() {
            let shapes: Vec<Box<dyn Shape>> = vec![
                Box::new(Circle { radius: 5.0 }),
                Box::new(Rectangle {
                    width: 3.0,
                    height: 4.0,
                }),
            ];
            assert!((total_area(&shapes) - 90.54).abs() < 0.01);
        }
    
        #[test]
        fn test_make_shape() {
            let s = make_shape("circle");
            assert_eq!(s.name(), "circle");
        }
    }

    Deep Comparison

    Core Insight

    Trait objects (dyn Trait) enable runtime polymorphism. The compiler generates a vtable for method dispatch. This is Rust's equivalent of OCaml's first-class modules or object system.

    OCaml Approach

  • • Object system with structural subtyping
  • • First-class modules for ad-hoc polymorphism
  • • No explicit vtable — runtime dispatch via method lookup
  • Rust Approach

  • dyn Trait behind a pointer (Box<dyn Trait>, &dyn Trait)
  • • Vtable-based dispatch (two-pointer fat pointer)
  • • Object safety rules: no generics, no Self in return position
  • Comparison Table

    FeatureOCamlRust
    Dynamic dispatchObjects / first-class modulesdyn Trait
    Pointer typeImplicit (GC)Box<dyn T> / &dyn T
    Type erasureYesYes (via vtable)
    OverheadMethod lookupFat pointer + vtable

    Exercises

  • Plugin system: Define a Plugin trait with name(&self) -> &str and execute(&self, input: &str) -> String. Build a PluginRegistry that stores Vec<Box<dyn Plugin>> and dispatches by name.
  • Object safety: Attempt to use Clone as a trait object: &dyn Clone. Observe the compiler error. Explain why Clone is not object-safe and how to work around it with a CloneBoxed trait.
  • Benchmark: Measure the performance difference between calling area via &dyn Shape (dynamic dispatch) vs fn area<T: Shape>(s: &T) (static dispatch) on 10M calls. Quantify the vtable overhead.
  • Open Source Repos