Why would I implement methods on a trait instead of as part of the trait?

When you define a trait named Foo that can be made into an object, Rust also defines a trait object type named dyn Foo. In older versions of Rust, this type was only called Foo (see What does “dyn” mean in a type?). For backwards compatibility with these older versions, Foo still works to name the trait object type, although dyn syntax should be used for new code.

Trait objects have a lifetime parameter that designates the shortest of the implementor’s lifetime parameters. To specify that lifetime, you write the type as dyn Foo + 'a.

When you write impl dyn Foo { (or just impl Foo { using the old syntax), you are not specifying that lifetime parameter, and it defaults to 'static. This note from the compiler on the y.foo_in_impl(); statement hints at that:

note: borrowed value must be valid for the static lifetime…

All we have to do to make this more permissive is to write a generic impl over any lifetime:

impl<'a> dyn Foo + 'a {
    fn foo_in_impl(&self) { println!("in impl") }
}

Now, notice that the self argument on foo_in_impl is a borrowed pointer, which has a lifetime parameter of its own. The type of self, in its full form, looks like &'b (dyn Foo + 'a) (the parentheses are required due to operator precedence). A Box<u8> owns its u8 – it doesn’t borrow anything –, so you can create a &(dyn Foo + 'static) out of it. On the other hand, &42u8 creates a &'b (dyn Foo + 'a) where 'a is not 'static, because 42u8 is put in a hidden variable on the stack, and the trait object borrows this variable. (That doesn’t really make sense, though; u8 doesn’t borrow anything, so its Foo implementation should always be compatible with dyn Foo + 'static… the fact that 42u8 is borrowed from the stack should affect 'b, not 'a.)

Another thing to note is that trait methods are polymorphic, even when they have a default implementation and they’re not overridden, while inherent methods on a trait objects are monomorphic (there’s only one function, no matter what’s behind the trait). For example:

use std::any::type_name;

trait Foo {
    fn foo_in_trait(&self)
    where
        Self: 'static,
    {
        println!("{}", type_name::<Self>());
    }
}

impl dyn Foo {
    fn foo_in_impl(&self) {
        println!("{}", type_name::<Self>());
    }
}

impl Foo for u8 {}
impl Foo for u16 {}

fn main() {
    let x = Box::new(42u8) as Box<dyn Foo>;
    x.foo_in_trait();
    x.foo_in_impl();

    let x = Box::new(42u16) as Box<Foo>;
    x.foo_in_trait();
    x.foo_in_impl();
}

Sample output:

u8
dyn playground::Foo
u16
dyn playground::Foo

In the trait method, we get the type name of the underlying type (here, u8 or u16), so we can conclude that the type of &self will vary from one implementer to the other (it’ll be &u8 for the u8 implementer and &u16 for the u16 implementer – not a trait object). However, in the inherent method, we get the type name of dyn Foo (+ 'static), so we can conclude that the type of &self is always &dyn Foo (a trait object).

Leave a Comment