RustArena

Step 5 of 7

Dyn compatibility (object safety)

Not every trait can be used as dyn Trait. To call methods through a vtable, the compiler must be able to build that table without knowing the concrete type. A trait that allows it is dyn compatible (older docs and blog posts call it object safe).

trait Shape {
    fn area(&self) -> f64;
    fn scaled(&self, k: f64) -> Self;
}

struct Circle {
    radius: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        3.0 * self.radius * self.radius
    }
    fn scaled(&self, k: f64) -> Self {
        Circle { radius: self.radius * k }
    }
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { radius: 1.0 })];
    println!("{}", shapes.len());
}
error[E0038]: the trait `Shape` is not dyn compatible
  |
  |     fn scaled(&self, k: f64) -> Self;
  |                                 ^^^^ ...because method `scaled` references the `Self` type in its return type

Through a dyn Shape, what would scaled return? A Circle? A Square? Its size isn't known at compile time, so there's no way to call it. The main rules, each with the same reason behind it:

The fixes

  1. Exclude a method from the vtable with where Self: Sized. It can still be called on concrete types, just not through dyn:
trait Shape {
    fn area(&self) -> f64;
    fn scaled(&self, k: f64) -> Self
    where
        Self: Sized;
}

struct Circle {
    radius: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        3.0 * self.radius * self.radius
    }
    fn scaled(&self, k: f64) -> Self {
        Circle { radius: self.radius * k }
    }
}

fn main() {
    let big = Circle { radius: 1.0 }.scaled(2.0);
    let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { radius: 1.0 }), Box::new(big)];
    for s in &shapes {
        println!("{}", s.area());
    }
}
3
12

Calling s.scaled(2.0) on a Box<dyn Shape> would now give error: the `scaled` method cannot be invoked on a trait object.

  1. Replace a generic parameter with a trait object: fn log(&self, value: &dyn Display) instead of fn log<T: Display>(&self, value: T).
  2. Return a box instead of Self: to clone trait objects, add fn clone_box(&self) -> Box<dyn Shape> and implement it as Box::new(self.clone()).
  3. Split the trait: keep the dyn-friendly methods in one trait and put constructors or generic helpers in another.

Your task

Widget can't be used as dyn Widget, so render_all doesn't compile. Make it dyn compatible without removing any method: change with_label to take label: &dyn Display instead of a generic, and keep duplicate callable on concrete widgets with where Self: Sized. Update both impls to match.

Challenge 2

Clone isn't dyn compatible, so you can't call .clone() on a Box<dyn Shape>. The standard workaround is a clone_box method. Circle already has it; implement clone_box for Square, and write fn duplicate_all(shapes: &[Box<dyn Shape>]) -> Vec<Box<dyn Shape>> that returns a copy of every shape.

Challenge 3

A key-value Storage trait should work as &mut dyn Storage, but it also has a constructor fn new() -> Self, which makes it not dyn compatible. Fix the trait (keep new, but exclude it from the vtable), implement it for MemoryStorage (backed by a HashMap<String, String>) and NullStorage (stores nothing; get always returns None), and write fn save_all(store: &mut dyn Storage, pairs: &[(&str, &str)]).

Previous: Trait objects in structs: plugins Next: Enums or trait objects?