Skip to content
Lesson 6 of 7

Step 1 of 7 · Reading · ~4 min

Learn

Lifetimes, Trait Objects, and Async

Trait Objects: What dyn Trait Really Is

You will be able to say what a Box<dyn Trait> is made of, predict which traits can be used as dyn, get the concrete type back out, and choose between dyn, generics and impl Trait.

In Traits and Generics every call was resolved at compile time, one copy of the code per type. Here the type is chosen at run time and the call goes through a vtable.

Fat pointers and the vtable

dyn Trait has no fixed size, so it lives behind a fat pointer: a data pointer plus a pointer to a vtable. The compiler emits one vtable per (type, trait) pair, holding the drop glue, size, alignment and one function pointer per method.

use std::fmt::Debug;
use std::mem::{size_of, size_of_val};

let small: Box<dyn Debug> = Box::new(7u8);
let big: Box<dyn Debug> = Box::new([0u64; 4]);
println!("{} {}", size_of::<&u8>(), size_of::<&dyn Debug>());       // 8 16
println!("{} {}", size_of::<Box<dyn Debug>>(), size_of::<&[u8]>()); // 16 16
println!("{} {}", size_of_val(&*small), size_of_val(&*big));        // 1 32

Sizes are for a 64-bit target. Both boxes have the same type, so the 1 and the 32 can only come from the vtable. A method call loads a slot and jumps through it: one indirect call that the optimizer usually cannot inline. &dyn T, Rc<dyn T> and Box<dyn Fn(i32) -> i32> work the same way.

Object safety

A vtable slot must be callable knowing only a pointer, so a method that needs the concrete type cannot have one:

trait Dup { fn dup(&self) -> Self; }
fn show(d: &dyn Dup) {}
error[E0038]: the trait `Dup` cannot be made into an object

dup returns Self, whose size the caller cannot know. A generic method would need one slot per instantiation, and a by-value self needs a size. Opt one method out of the vtable with where Self: Sized and the rest of the trait stays usable as dyn:

trait Sink {
    fn put(&mut self, line: &str);
    fn put_all<I: Iterator<Item = String>>(&mut self, it: I) where Self: Sized {
        for s in it { self.put(&s); }
    }
}

put_all now exists on concrete types only. Rust 1.40 also cannot turn a &dyn Widget into a &dyn Debug even when Widget: Debug (upcasting arrived in 1.86).

Getting the type back: dyn Any

Because of that, a trait exposes Any itself:

use std::any::Any;
trait Widget { fn as_any(&self) -> &dyn Any; }
struct Button { clicks: u32 }
struct Label;
impl Widget for Button { fn as_any(&self) -> &dyn Any { self } }
impl Widget for Label { fn as_any(&self) -> &dyn Any { self } }

let w: Box<dyn Widget> = Box::new(Button { clicks: 2 });
println!("{:?}", w.as_any().downcast_ref::<Button>().map(|b| b.clicks)); // Some(2)
println!("{}", w.as_any().downcast_ref::<Label>().is_none());           // true

downcast_ref compares type ids and answers with an Option. Any needs 'static types, which matches the default: Box<dyn Widget> in a field or return type means Box<dyn Widget + 'static>. To hold borrows write Box<dyn Widget + 'a>.

Consuming receivers. A by-value self cannot be called on dyn, but self: Box<Self> can, because the box has a known size. A method fn push(self: Box<Self>) -> Box<dyn Door> lets one state consume itself and return the next, which is the state pattern.

Box<dyn Error>: one opaque error type

? converts with From, and the standard library has From<E: Error> for Box<dyn Error>, so unrelated error types share one return type. A String or &str converts with .into().

use std::{error::Error, fmt};
#[derive(Debug)] struct BadPort(u32);
impl fmt::Display for BadPort {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "port {} out of range", self.0) }
}
impl Error for BadPort {}

fn port(text: &str) -> Result<u16, Box<dyn Error>> {
    let n: u32 = text.trim().parse()?;
    if n == 0 || n > 65535 { return Err(Box::new(BadPort(n))); }
    Ok(n as u16)
}
// port("8080") is Ok(8080); port("80x") fails with "invalid digit found in string";
// port("70000") fails with "port 70000 out of range"

The cost against the error enum from Result/Option chaining: callers cannot match on variants, only ask e.downcast_ref::<BadPort>(). Use an enum in a library whose callers branch on the failure, Box<dyn Error> in an application that only reports it.

Choose generics for hot paths with one type per call site, dyn for a Vec of mixed types, a type picked from input, or a plugin boundary, and impl Trait for one hidden type with no vtable.

Common mistakes

  • Printing an error with {:?}. A ParseIntError prints as ParseIntError { kind: InvalidDigit }; {} prints the message.
  • Letting arithmetic panic. Plain * overflows into a panic and loses every later output; use checked_* and return an error.
  • Vec<dyn Trait> without the Box. The element is unsized.
  • Reporting add instead of add 5. The failing stage must be identifiable.

Your exercise

Build a stage pipeline from a spec line. The starter reads and prints; you write the Stage impls and build. The likeliest mistake is a stage that keeps running after an earlier one failed.

Up nextAsync/await: Futures, Wakers and ExecutorsLifetimes, Trait Objects, and Async

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…