dyn safety (object safety)

There exists traits for which you cannot create a dyn Trait:

#![allow(unused)]
fn main() {
let s = String::new();
let d: &dyn Clone = &s;
}

Instead of repeating all the rules here, I'll just link to the reference. You should go read that first.

Note that as of this writing, the reference hasn't been updated to document that you can opt to make associated types and GATs unavailable to trait objects by adding a where Self: Sized bound. For now I'll refer to this as opting the GAT (or associated type) out of being "dyn-usable".

What may not be immediately apparent is why these limitations exists. The rest of this page explores some of the reasons.

The Sized constraints

Before we get into the restrictions, let's have an aside about how the Sized constraints work with dyn Trait and dyn safety.

Rust uses Sized to indicate that

  • A trait is not dyn safe
  • An associated type or GAT is not dyn-usable
  • A method is not dyn-dispatchable
  • An associated function is not callable for dyn Trait
    • Even though it never can be (so far), you have to declare this for the sake of being explicit and for potential forwards compatibility

This makes some sense, as dyn Trait is not Sized. So a dyn Trait cannot implement a trait with Sized as a supertrait, and a dyn Trait can't call methods (or associated functions) that require Sized either.

However, it's still a hack as there are types which are not Sized but also not dyn Trait, and we might want to implement our trait for those, including some methods which are not dyn-dispatchable (such as generic methods). Currently that's just not possible in Rust (the non-dyn-dispatchable methods will also not be available for other unsized types).

The next few paragraphs demonstrate (or perhaps rant about) how this can be an annoying limitation. If you'd rather get on with learning practical Rust, you may want to skip ahead 🙂.

Consider this example, where we've added a Sized bound in order to remain a dyn-safe trait:

#![allow(unused)]
fn main() {
trait Bound<T: ?Sized> {}
trait Trait {
    // Opt-out of `dyn`-dispatchability for this method because it's generic
    fn method<T: Bound<Self>>(&self) where Self: Sized;
}
}

If you try to implement this trait for str, you won't have method available, even if it would logically make sense to have it available. Moreover, if you write the implementation like so:

#![allow(unused)]
fn main() {
trait Bound<T: ?Sized> {}
trait Trait {
   fn method<T: Bound<Self>>(&self) where Self: Sized;
}
impl Trait for str {
    // `Self: Sized` isn't true, so don't bother with `method`
}
}

You get an error saying you must provide method, even though the bounds cannot be satisfied. So then you can provide a perfectly functional implementation:

#![allow(unused)]
fn main() {
trait Bound<T: ?Sized> {}
trait Trait {
   fn method<T: Bound<Self>>(&self) where Self: Sized;
}
impl Trait for str {
    fn method<T: Bound<Self>>(&self) where Self: Sized {
        // do logical `method` things
    }
}
}

Whoops, it doesn't accept that either! 😠 We have to implement it without the bound, like so:

#![allow(unused)]
fn main() {
trait Bound<T: ?Sized> {}
trait Trait {
   fn method<T: Bound<Self>>(&self) where Self: Sized;
}
impl Trait for str {
    fn method<T: Bound<Self>>(&self) {
        // do logical `method` things
    }
}
}

And that compiles... but we can never actually call it.

trait Bound<T: ?Sized> {}
trait Trait {
   fn method<T: Bound<Self>>(&self) where Self: Sized;
}
impl Trait for str {
    fn method<T: Bound<Self>>(&self) {
    }
}
fn main() {
    "".method();
}

Alternatively, we can exploit the fact that higher-ranked bounds are checked at the call site and not the definition site to sneak in the unsatisfiable Self: Sized bound in a way that compiles:

#![allow(unused)]
fn main() {
trait Bound<T: ?Sized> {}
trait Trait {
   fn method<T: Bound<Self>>(&self) where Self: Sized;
}
impl Trait for str {
    // Still not callable, but compiles:   vvvvvvv  due to this binder
    fn method<T: Bound<Self>>(&self) where for<'a> Self: Sized {
        unreachable!()
    }
}
}

But naturally the method still cannot be called, as the bound is not satisfiable.

This is a pretty sad state of affairs. Ideally, there would be a distinct trait for opting out of dyn safety and dispatchability instead of using Sized for this purpose; let's call it NotDyn. Then we could have Sized: NotDyn for backwards compatibility, change the bound above to be NotDyn, and have our implementation for str be functional.

There also some other future possibilities that may improve the situation:

  • Some resolution of RFC issue 2829 or the duplicates linked within would allow omitting the method altogether (but it would still not be callable)
  • RFC 2056 will allow defining the method with the trivially unsatifiable bound without exploiting the higher-ranked trick (but it will still not be callable)
  • RFC 3245 will allow calling <str as Trait>::method and refined implementations more generally

But I feel removing the conflation between dyn safety and Sized would be more clear and correct regardless of any future workarounds that may exist.

Receiver limitations

The requirement for some sort of Self-based receiver on dyn-dispatchable methods is to ensure the vtable is available. Some wide pointer to Self needs to be present in order to find the vtable and perform dynamic dispatch.

Arguably this could be expanded to methods that take a single, non-receiver &Self and so on.

As for the other limitation on receiver types, the compiler has to know how to go backwards from type erased version to original version in order to implement Trait. This may be generalized some day, but for now it's a restricted set.

Generic method limitations

In order to support type-generic methods, there would need to be a function pointer in the vtable for every possible type that the generic could take on. Not only would this create vtables of unwieldly size, it would also require some sort of global analysis. After all, every crate which uses your trait might define new types that meet the trait bounds in question, and they (or you) might also want to call the method using those types.

You can sometimes work around this limitation by type erasing the generic type parameter in question (in the main method, as an alternative method, or in a different "erased" trait). We'll see an example of this later.

Use of Self limitations

Methods which take some form of Self other than as a receiver can depend on the parameter being exactly the same as the implementing type. But this can't be relied upon once the base types have been erased.

For example, consider PartialEq<Self>:

#![allow(unused)]
fn main() {
// Simplified
pub trait PartialEq {
    fn partial_eq(&self, rhs: &Self);
}
}

If this were implemented for dyn PartialEq, the rhs parameter would be a &dyn PartialEq like self is. But there is no guarantee that the base types are the same! Both u8 and String implement PartialEq for example, but there's no facility to compare them for equality (and Rust has no interest in handling this in an arbitrary way).

You can sometimes work around this by supplying your own implementations for some other dyn Trait, perhaps utilizing the Any trait to emulate dynamic typing and reflection. We give an example of this approach later.

The impl Clone for Box<dyn Trait> example demonstrates handling a case where Self is the return value.

GAT limitations

GATs are too new to support type erasing as-of-yet. We'll need some way to embed the GAT into the dyn Trait as a parameter, similar to how is done for non-generic associated types.

As of Rust 1.72, you can opt out of GATs being dyn-usable, and thus out of the necessity of naming the GAT as a parameter, by adding a Self: Sized bound.

This is similar to the same ability on non-generic associated types. Interestingly, it allows specifying not only specific GAT equalities...

#![allow(unused)]
fn main() {
trait Trait {
    type Gat<'a> where Self: Sized;
}

impl Trait for () {
    type Gat<'a> = &'a str;
}

let _: &dyn Trait<Gat<'static> = &'static str> = &();
}

...but also higher-ranked GAT equality:

#![allow(unused)]
fn main() {
trait Trait {
   type Gat<'a> where Self: Sized;
}
impl Trait for () {
   type Gat<'a> = &'a str;
}
// This syntax is still not supported
// let _: &dyn Trait<for<'a> Gat<'a> = &'a str> = &();

// However, with `dyn Trait`, you can move the binder to outside the `Trait`:
let _: &dyn for<'a> Trait<Gat<'a> = &'a str> = &();
}

However, as with the non-generic associated type case, making any use of the equality would have to be done indirectly, as the dyn Trait itself cannot define a GAT in its own implementation.

Associated constant limitations

Similarly, supporting associated constants will require at least support for associated constant equality.

Return position impl Trait limitations

Trait methods utilizing RPITs are, notionally at least, sugar for declaring an opaque associated type or generic associated type. Additionally, even if the RPIT captures no generic parameters and thus corresponds to returning an associated type, there is currently no way to name that associated type.

Similar to generic methods, you can sometimes work around this limitation by type erasing the return type. (Note that there are some trade-offs, but a discussion of such is more suited to a dedicated guide about RPITs.)

History

Object safety was introduced in RFC 0255, and RFC 0546 removed the implied Sized bound on traits and added the rule that traits with (explicit) Sized bounds are not object safe.

Both RFCs were implemented before Rust 1.0.