dyn Trait and dynamic dispatch

In Rust a trait object is a value whose concrete type is known only at run time. It is written dyn Trait, and it is always behind a pointer: &dyn Trait or Box<dyn Trait>. The pointer carries the data and a table of the trait methods, and a call goes through that table.

The previous lesson chose the implementation at compile time. This one chooses at run time, which is what a vector of mixed shapes needs. The price is one pointer indirection per call and a value that cannot be sized by the compiler.

&dyn Trait

A parameter of type &dyn Area accepts a reference to any implementor, and the compiler keeps the concrete type out of the signature. The call inside the function goes through the vtable, so the same code runs both implementations.

The program passes a square and a circle to the same function. The output is the two areas in call order, and the function never names either type.

trait Area { fn area(&self) -> f64; } struct Square(f64); struct Circle(f64); impl Area for Square { fn area(&self) -> f64 { self.0 * self.0 } } impl Area for Circle { fn area(&self) -> f64 { 3.14159 * self.0 * self.0 } } fn report(shape: &dyn Area) { println!("{:.2}", shape.area()); } fn main() { report(&Square(3.0)); report(&Circle(1.0)); }

rustc --edition 2024 dyn_ref.rs
./dyn_ref

9.00
3.14

The reference must outlive the call, which the borrow checker checks as usual. The lesson on traits shows the generic version of the same function, which has no vtable at all.

Box<dyn Trait>

An owned trait object lives in a Box, which puts the value on the heap and keeps the pointer. A vector of Box<dyn Area> can hold a square and a circle together even though the two have different sizes.

The program sums the areas of both shapes stored in the vector. The loop calls the method on each box, and the total is the sum of the two areas with two decimals.

trait Area { fn area(&self) -> f64; } struct Square(f64); struct Circle(f64); impl Area for Square { fn area(&self) -> f64 { self.0 * self.0 } } impl Area for Circle { fn area(&self) -> f64 { 3.14159 * self.0 * self.0 } } fn main() { let shapes: Vec<Box<dyn Area>> = vec![Box::new(Square(3.0)), Box::new(Circle(1.0))]; let mut total = 0.0; for shape in &shapes { total += shape.area(); } println!("{:.2}", total); }

rustc --edition 2024 boxed.rs
./boxed

12.14

Each Box::new allocates once, and the vector stores the pointers. The lesson on Box covers the ownership of the allocation and when it is freed.

Returning a trait object

A function that returns a different concrete type on different paths returns a boxed trait object, because the concrete type is not known to the caller. The signature names the trait and the box, not the implementation.

The program builds a square through such a function and prints its area. The caller cannot tell which type is inside, which is the point and also the limitation.

trait Area { fn area(&self) -> f64; } struct Square(f64); impl Area for Square { fn area(&self) -> f64 { self.0 * self.0 } } fn make() -> Box<dyn Area> { Box::new(Square(3.0)) } fn main() { let shape = make(); println!("{:.2}", shape.area()); }

rustc --edition 2024 make.rs
./make

9.00

Returning impl Trait would keep the concrete type and avoid the box, but the function could then return only one type on every path. The lesson on trait bounds uses that form for a single type.

Static against dynamic

A generic function with a trait bound is monomorphised: the compiler writes one version per type, and the call is direct. A dyn call reads the method from the vtable at run time. Both accept the same implementors, and they differ in code size and indirection.

The program calls both forms with two types and prints the names. The output is the same, and the difference is invisible at the source level.

trait Name { fn name(&self) -> &'static str; } struct A; struct B; impl Name for A { fn name(&self) -> &'static str { "A" } } impl Name for B { fn name(&self) -> &'static str { "B" } } fn static_call<T: Name>(value: &T) -> &'static str { value.name() } fn dynamic_call(value: &dyn Name) -> &'static str { value.name() } fn main() { println!("{} {}", static_call(&A), dynamic_call(&B)); }

rustc --edition 2024 dispatch.rs
./dispatch

A B

The static form is the default choice because it is faster and the type is known. The dynamic form is for a collection of mixed types or a public API that should not expose the concrete type.

A trait that is not dyn compatible

A method that returns Self cannot be called through a trait object, because the caller would not know the size of the result. A trait with such a method is not dyn compatible, and putting it behind dyn is E0038.

The program below has a method that returns Self and tries to box the trait. The message names the method and the reason, which is the rule in one sentence.

trait Cloneable { fn duplicate(&self) -> Self; } struct A; impl Cloneable for A { fn duplicate(&self) -> Self { A } } fn main() { let value = A; let boxed: Box<dyn Cloneable> = Box::new(value); println!("{}", boxed.duplicate().duplicate()); }

rustc --edition 2024 not_safe_err.rs

error[E0038]: the trait `Cloneable` is not dyn compatible
--> not_safe_err.rs:15:24
|
15 | let boxed: Box<dyn Cloneable> = Box::new(value);
| ^^^^^^^^^ `Cloneable` is not dyn compatible
|
note: for a trait to be dyn compatible it needs to allow building a vtable
for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
--> not_safe_err.rs:2:28
|
1 | trait Cloneable {
| --------- this trait is not dyn compatible...
2 | fn duplicate(&self) -> Self;
| ^^^^ ...because method `duplicate` references the `Self` type in its return type
= help: consider moving `duplicate` to another trait
= help: only type `A` implements `Cloneable`; consider using it directly instead.

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0038`.

The fix is a method that returns a fixed type, or a separate object-safe trait for the part that is called dynamically. The standard library splits traits for exactly this reason.

Exercises

  1. Add a triangle to the vector of boxed shapes and print the total area again.
  2. Change the report function from &dyn Area to a generic bound and compare the two signatures.
  3. Write a function that returns a boxed trait object and a second one that returns impl Trait. Which one can return two different types?

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What must a trait object always be behind?

    Answer

    A pointer, such as a reference or a Box

    The concrete type has no known size, so the trait object is used through a pointer.

  2. When is the implementation of a dyn call chosen?

    Answer

    At run time, through the vtable

    A trait object carries a table of methods, and the call reads the right one at run time.

  3. What does Box<dyn Area> allow that &dyn Area does not?

    Answer

    Owning the value and storing mixed types in a vector

    The box owns the value, so the trait object can live in a collection without a borrowed owner.

  4. What makes a trait not dyn compatible?

    Answer

    A method whose signature uses Self by value or returns Self

    The vtable cannot return a type whose size it does not know.

  5. What does this program print? trait Area { fn area(&self) -> f64; } struct Square(f64); struct Circle(f64); impl Area for Square { fn area(&self) -> f64 { self.0 * self.0 } } impl Area for Circle { fn area(&self) -> f64 { 3.14159 * self.0 * self.0 } } fn report(shape: &dyn Area) { println!("{:.2}", shape.area()); } fn main() { report(&Square(3.0)); report(&Circle(1.0)); }

    Answer

    9.00 / 3.14

    Both references become trait objects, and each call runs the matching implementation.

  6. What does this program print? trait Area { fn area(&self) -> f64; } struct Square(f64); struct Circle(f64); impl Area for Square { fn area(&self) -> f64 { self.0 * self.0 } } impl Area for Circle { fn area(&self) -> f64 { 3.14159 * self.0 * self.0 } } fn main() { let shapes: Vec<Box<dyn Area>> = vec![Box::new(Square(3.0)), Box::new(Circle(1.0))]; let mut total = 0.0; for shape in &shapes { total += shape.area(); } println!("{:.2}", total); }

    Answer

    12.14

    The boxed objects share one type in the vector, and their areas add up to twelve point one four.

  7. What does this program print? trait Area { fn area(&self) -> f64; } struct Square(f64); impl Area for Square { fn area(&self) -> f64 { self.0 * self.0 } } fn make() -> Box<dyn Area> { Box::new(Square(3.0)) } fn main() { let shape = make(); println!("{:.2}", shape.area()); }

    Answer

    9.00

    The box carries the square and its implementation, and the call returns nine.

  8. What does this program print? trait Name { fn name(&self) -> &'static str; } struct A; struct B; impl Name for A { fn name(&self) -> &'static str { "A" } } impl Name for B { fn name(&self) -> &'static str { "B" } } fn pick(value: &dyn Name) -> &'static str { value.name() } fn main() { println!("{} {}", pick(&A), pick(&B)); }

    Answer

    A B

    The two trait objects each run their own name method, so the output is A then B.

  9. What is the first line of the compiler output for this program? trait Cloneable { fn duplicate(&self) -> Self; } struct A; impl Cloneable for A { fn duplicate(&self) -> Self { A } } fn main() { let boxed: Box<dyn Cloneable> = Box::new(A); println!("{}", boxed.duplicate().duplicate()); }

    Answer

    error[E0038]: the trait Cloneable is not dyn compatible

    The method returns Self, so the trait cannot be turned into an object with a vtable.

  10. What does this program print? trait Name { fn name(&self) -> &'static str; } struct A; impl Name for A { fn name(&self) -> &'static str { "A" } } fn static_call<T: Name>(value: &T) -> &'static str { value.name() } fn main() { println!("{}", static_call(&A)); }

    Answer

    A

    The generic function is monomorphised for A, and the call is direct.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev