dyn Trait Overview

What is dyn Trait?

dyn Trait is a compiler-provided type which implements Trait. Any Sized implementor of Trait can be coerced to be a dyn Trait, erasing the original base type in the process. Different implementations of Trait may have different sizes, and as a result, dyn Trait has no statically known size. That means it does not implement Sized, and we call such types "unsized", or "dynamically sized types (DSTs)".

Every dyn Trait value is the result of type erasing some other existing value. You cannot create a dyn Trait from a trait definition alone; there must be an implementing base type that you can coerce.

Rust currently does not support passing unsized parameters, returning unsized values, or having unsized locals. Therefore, when interacting with dyn Trait, you will generally be working with some sort of indirection: a Box<dyn Trait>, &dyn Trait, Arc<dyn Trait>, etc.

And in fact, the indirection is necessary for another reason. These indirections are or contain wide pointers to the erased type, which consist of a pointer to the value, and a second pointer to a static vtable. The vtable in turn contains data such as the size of the value, a pointer to the value's destructor, pointers to methods of the Trait, and so on. The vtable enables dynamic dispatch, by which different dyn Trait values can dispatch method calls to the different erased base type implementations of Trait.

dyn Trait is also called a "trait object".

You can also have objects such as dyn Trait + Send + Sync. Send and Sync are auto-traits, and a trait object can include any number of these auto traits as additional bounds. Every distinct set of Trait + AutoTraits is a distinct type.

However, you can only have one non-auto trait in a trait object, so this will not work:

#![allow(unused)]
fn main() {
trait Trait1 {}
trait Trait2 {};
struct S(Box<dyn Trait1 + Trait2>);
}

That being noted, one can usually use a subtrait/supertrait pattern to work around this restriction.

The trait object lifetime

Confession: we were being imprecise when we said dyn Trait is a type. dyn Trait is a type constructor: it is parameterized with a lifetime, similar to how references are. So dyn Trait on it's own isn't a type, dyn Trait + 'a for some concrete lifetime 'a is a type.

The lifetime can usually be elided, which we will explore later. But it is always part of the type, just like a lifetime is part of every reference type, even when elided.

Associated Types

If a trait has non-generic associated types, those associated types usually become named parameters of dyn Trait:

#![allow(unused)]
fn main() {
let _: Box<dyn Iterator<Item = i32>> = Box::new([1, 2, 3].into_iter());
}

We explore associated types in dyn Trait more in a later section.

What dyn Trait is not

dyn Trait is not Sized

We mentioned the fact that dyn Trait is not Sized already.

However, let us take a moment to note that generic parameters have an implicit Sized bound. Therefore you may need to remove the implicit bound by using : ?Sized in order to use dyn Trait in generic contexts.

#![allow(unused)]
fn main() {
trait Trait {}
// This function only takes `T: Sized`.  It cannot accept a
// `&dyn Trait`, for example, as `dyn Trait` is not `Sized`.
fn foo<T: Trait>(_: &T) {}

// This function takes any `T: Trait`, even if `T` is not
// `Sized`.
fn bar<T: Trait + ?Sized>(t: &T) {
    // Demonstration that `foo` cannot accept non-`Sized`
    // types:
    foo(t);
}
}

dyn Trait is neither a generic nor dynamically typed

Given a concrete lifetime 'a, dyn Trait + 'a is a statically known type. The erased base type is not statically known, but don't let this confuse you: the dyn Trait itself is its own distinct type and that type is known at compile time.

For example, consider these two function signatures:

#![allow(unused)]
fn main() {
trait Trait {}
fn generic<T: Trait>(_rt: &T) {}
fn not_generic(_dt: &dyn Trait) {}
}

In the generic case, a distinct version of the function will exist for every type T which is passed to the function. This compile-time generation of new functions for every type is known as monomorphization. (Side note, lifetimes are erased during compilation, and not monomorphized.)

You can even create function pointers to the different versions like so:

trait Trait {}
impl Trait for String {}
fn generic<T: Trait>(_rt: &T) {}
fn main() {
let fp = generic::<String>;
}

That is, the function item type is parameterized by some T: Trait.

In contrast, there will always only be only one non_generic function in the resulting library. The base implementors of Trait must be typed-erased into dyn Trait + '_ before being passed to the function. The function type is not parameterized by a generic type.

Similarly, here:

#![allow(unused)]
fn main() {
trait Trait {}
fn generic<T: Trait>(bx: Box<T>) {}
}

bx: Box<T> is not a Box<dyn Trait>. It is a thin owning pointer to a heap allocated T specifically. Because T has an implicit Sized bound here, we could coerce bx to a Box<dyn Trait + '_>. But that would be a transformation to a different type of Box: a wide owning pointer which has erased T and included the corresponding vtable pointer.

We'll explore more details on the interaction of generics and dyn Trait in a later section.

You may wonder why you can use the methods of Trait on a &dyn Trait or Box<dyn Trait>, etc., despite not declaring any such bound. The reason is analogous to why you can use Display methods on a String without declaring that bound, say: the type is statically known, and the compiler recognizes that dyn Trait implements Trait, just like it recognizes that String implements Display. Trait bounds are needed for generics, not concrete types.

(In fact, Box<dyn Trait> doesn't implement Trait automatically, but deref coercion usually takes care of that case. For many std traits, the trait is explicitly implemented for Box<dyn Trait> as well; we'll also explore what that can look like.)

As a concrete type, you can also implement methods on dyn Trait (provided Trait is local to your crate), and even implement other traits for dyn Trait (as we will see in some of the examples).

dyn Trait is not a supertype

Because you can coerce base types into a dyn Trait, it is not uncommon for people to think that dyn Trait is some sort of supertype over all the coercible implementors of Trait. The confusion is likely exacerbated by trait bounds and lifetime bounds sharing the same syntax.

But the coercion from a base type to a dyn Trait is an unsizing coercion, and not a sub-to-supertype conversion; the coercion happens at statically known locations in your code, and may change the layout of the types involved (e.g. changing a thin pointer into a wide pointer) as well.

Relatedly, trait Trait is not a class. You cannot create a dyn Trait without an implementing type (they do not have built-in constructors), and a given type can implement a great many traits. Due to the confusion it can cause, I recommend not referring to base types as "instances" of the trait. It is just a type that implements Trait, which exists independently of the trait. When I create a String, I'm creating a String, not "an instance of Display (and Debug and Write and ToString and ...)".

When I read "an instance of Trait", I assume the variable in question is some form of dyn Trait, and not some unerased base type that implements Trait.

Implementing something for dyn Trait does not implement it for all other T: Trait. In fact it implements it for nothing but dyn Trait itself. Implementing something for dyn Trait + Send doesn't implement anything for dyn Trait or vice-versa either; those are also separate, distinct types.

There are ways to emulate dynamic typing in Rust, which we will explore later. We'll also explore the role of supertraits (which, despite the name, still do not define a sub/supertype relationship).

The only subtypes in Rust involve lifetimes and types which are higher-ranked over lifetimes.

(Pedantic self-correction: trait objects have lifetimes and thus are supertypes in that sense. However that's not the same concept that most Rust learners get confused about; there is no supertype relationship with the implementing types.)

dyn Trait is not universally applicable

We'll look at the details in their own sections, but in short, you cannot always coerce an implementor of Trait into dyn Trait. Both the trait and the implementor must meet certain conditions.

In summary

dyn Trait + 'a is

  • a concrete, statically known type
  • created by type erasing implementors of Trait
  • used behind wide pointers to the type-erased value and to a static vtable
  • dynamically sized (unsized, does not implement Sized)
  • an implementor of Trait via dynamic dispatch
  • not a supertype of all implementors
  • not dynamically typed
  • not a generic
  • not creatable from all values
  • not available for all traits