386: Object-Safe Traits
Difficulty: 3 Level: Advanced Only object-safe traits can be used as `dyn Trait` โ the rules determine which ones qualify.The Problem This Solves
Sometimes you don't know the concrete type at compile time. You want a `Vec<Box<dyn Shape>>` that holds circles, rectangles, and triangles together โ heterogeneous collections with runtime dispatch. For this, Rust needs to build a vtable: a table of function pointers, one per trait method. The vtable is attached to a fat pointer (`dyn Trait = data ptr + vtable ptr`). Not every trait can have a vtable. If a method returns `Self` (the concrete type), the vtable can't know what size to allocate โ different types have different sizes. If a method is generic over `T`, there's no single function pointer to store; every `T` produces a different monomorphized version. These traits are not object-safe and cannot be used as `dyn Trait`. Understanding the rules lets you design traits that are usable both statically (generics) and dynamically (trait objects), choosing the right tool for plugin systems, callbacks, and heterogeneous collections.The Intuition
Object safety is about whether the compiler can build a vtable for your trait. A vtable is a fixed-size array of function pointers. That means each method must have exactly one version and must work through a pointer without knowing the concrete type. The two main violations: `-> Self` (can't know the return type size) and `fn method<T>` (would need infinite vtable entries, one per `T`). The workaround for `-> Self` is to return a `Box<dyn Trait>` instead, or gate the method with `where Self: Sized` to exclude it from the vtable.How It Works in Rust
// Object-SAFE: no Self returns, no generic methods
trait Drawable {
fn draw(&self);
fn area(&self) -> f64;
// This method is excluded from the vtable via where Self: Sized
// It's available on concrete types but not through dyn Drawable
fn clone_box(&self) -> Box<dyn Drawable> where Self: Sized + Clone {
Box::new(self.clone())
}
}
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }
impl Drawable for Circle {
fn draw(&self) { println!("Circle(r={})", self.radius); }
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
impl Drawable for Rectangle {
fn draw(&self) { println!("Rectangle({}x{})", self.width, self.height); }
fn area(&self) -> f64 { self.width * self.height }
}
// Heterogeneous collection โ works because Drawable is object-safe
fn total_area(shapes: &[Box<dyn Drawable>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
fn main() {
let shapes: Vec<Box<dyn Drawable>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle { width: 4.0, height: 6.0 }),
];
println!("Total area: {:.2}", total_area(&shapes));
}
What makes a trait NOT object-safe:
trait NotObjectSafe {
fn clone_self(&self) -> Self; // returns Self โ size unknown
fn map<T>(&self, f: fn(f64) -> T); // generic method โ infinite vtable entries
}
// let _: &dyn NotObjectSafe; // COMPILE ERROR
What This Unlocks
- Plugin systems โ define a `dyn Plugin` interface; load and call plugins without knowing their concrete types.
- Heterogeneous collections โ `Vec<Box<dyn Drawable>>`, `Vec<Box<dyn Handler>>` โ mix types that share a behavior.
- Callback registries โ store `Vec<Box<dyn Fn(Event)>>` where callers register different closures.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Heterogeneous dispatch | Class types (`drawable list`) โ runtime via object method table | `Vec<Box<dyn Drawable>>` โ fat pointer with vtable |
| Object safety rules | Implicit โ OCaml OO always dispatches dynamically | Explicit โ trait must satisfy rules; compiler rejects violations |
| Opt-out for unsafe methods | No direct mechanism | `where Self: Sized` excludes a method from the vtable |
| Cost | Heap allocation + vtable (same) | Heap allocation + vtable (explicit via `Box`) |
// Object safety rules in Rust
use std::fmt;
// Object-SAFE trait: no Self returns, no generics
trait Drawable {
fn draw(&self);
fn area(&self) -> f64;
// This method is excluded from vtable with where Self: Sized
fn clone_box(&self) -> Box<dyn Drawable> where Self: Sized + Clone {
Box::new(self.clone())
}
}
// NOT object safe if it had:
// fn clone_self(&self) -> Self; // returns Self
// fn map<T, F: Fn(f64) -> T>(&self, f: F) -> T; // generic method
#[derive(Clone)]
struct Circle { radius: f64 }
#[derive(Clone)]
struct Rectangle { width: f64, height: f64 }
impl Drawable for Circle {
fn draw(&self) { println!("Circle(r={})", self.radius); }
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
impl Drawable for Rectangle {
fn draw(&self) { println!("Rectangle({}x{})", self.width, self.height); }
fn area(&self) -> f64 { self.width * self.height }
}
impl fmt::Display for Circle {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Circle({})", self.radius) }
}
fn total_area(shapes: &[Box<dyn Drawable>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
fn main() {
let shapes: Vec<Box<dyn Drawable>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle { width: 4.0, height: 6.0 }),
Box::new(Circle { radius: 2.0 }),
];
for s in &shapes {
s.draw();
println!(" area = {:.2}", s.area());
}
println!("Total area: {:.2}", total_area(&shapes));
// Demonstrating object safety check at compile time
// The next line would fail to compile:
// let _: &dyn fmt::Display = &Circle { radius: 1.0 }; // Display IS object safe
println!("Display is object safe: {:?}",
format!("{}", Circle { radius: 3.0 }));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_area() {
let c = Circle { radius: 1.0 };
assert!((c.area() - std::f64::consts::PI).abs() < 1e-9);
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 Drawable>> = vec![
Box::new(Rectangle { width: 2.0, height: 3.0 }),
];
assert_eq!(total_area(&shapes), 6.0);
}
}
(* Object safety concepts in OCaml using class types *)
(* Object-safe equivalent: method returns unit, no generics *)
class type drawable = object
method draw : unit
method area : float
end
class circle r = object
method draw = Printf.printf "Drawing circle with radius %.1f\n" r
method area = Float.pi *. r *. r
end
class rectangle w h = object
method draw = Printf.printf "Drawing %g x %g rectangle\n" w h
method area = w *. h
end
let total_area (shapes : drawable list) =
List.fold_left (fun acc s -> acc +. s#area) 0.0 shapes
let () =
let shapes : drawable list = [
new circle 5.0;
new rectangle 4.0 6.0;
new circle 2.0;
] in
List.iter (fun s -> s#draw) shapes;
Printf.printf "Total area: %.2f\n" (total_area shapes)