dyn Trait coercions

Some dyn Trait coercions which are typical (in terms of what is being coerced) look like so:

#![allow(unused)]
fn main() {
use std::sync::Arc;
trait Trait {}
fn coerce_ref<'a, T: Trait + Sized + 'a>(t:    &T ) -> &(  dyn Trait + 'a) { t }
fn coerce_box<'a, T: Trait + Sized + 'a>(t: Box<T>) -> Box<dyn Trait + 'a> { t }
fn coerce_arc<'a, T: Trait + Sized + 'a>(t: Arc<T>) -> Arc<dyn Trait + 'a> { t }
// etc
}

These are more syntactically noisy than you will typically see in practice, as I have included some explicit lifetimes and bounds which are normally implied or not used. For example the Sized bound on generic type parameters is usually implied, but I've made it explicit to emphasize that we're talking about Sized base types.

The key point is that given an object safe Trait, and when T: 'a + Trait + Sized, you can coerce a Ptr<T> to a Ptr<dyn Trait + 'a> for the supported Ptr pointer types such as &_ and Box<_>.

If we had wanted a dyn Trait + Send + 'a, naturally we would need T: Send as well, and similarly for any other auto trait.

In the rest of this section, we look at cases beyond these typical examples, as well as some limitations of coercions.

Associated types

When a trait has one or more non-generic associated type, every concrete implementor of the trait chooses a single, statically-known type for each associated type. For base types, this means the associated types are "outputs" of the implementing type and the implemented trait: if you know the latter two, you can statically determine the associated types as well.

So what should the associated types be in the implementation of Trait for dyn Trait?

There is no single answer; they would need to vary based on the erased base types.

However, dyn Trait for traits with associated types is just too useful to make traits with associated types ineligible for dyn Trait. Instead, associated types in the trait become, in essence, named type parameters of the dyn Trait type constructor. (Recall it's already a type constructor due to the trait object lifetime.)

So given

#![allow(unused)]
fn main() {
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}
}

We have

dyn Iterator<Item = String> + '_
dyn Iterator<Item = i32> + '_
dyn Iterator<Item = f64> + '_

and so on. The associated types in dyn Trait<...> must be resolved to concrete types in order for the dyn Trait<...> to be a concrete type.

Naturally, you can only coerce to dyn Iterator<Item = String> if you both implement Iterator, and in your implementation, type Item = String. The syntax mirrors that of associated type trait bounds:

#![allow(unused)]
fn main() {
fn takes_string_iter<Iter>(i: Iter)
where
    Iter: Iterator<Item = String>,
{
   // ...
}
}

The parameters being named has a number of benefits. For one, it's usually quite relevant, such as what Item an Iterator returns (especially if the associated types are well named). It also removes the need to order the associated types in a well-defined way, such as lexicographically or especially declaration order (which would be too fragile).

The named parameters must be specified after all ordered parameters, however.

#![allow(unused)]
fn main() {
trait AssocAndParams<T, U> { type Assoc1; type Assoc2; }

// The trait's ordered type parameters must be in declaration order
// (here, `String` then `usize`).  After that come the named associated
// type parameters, which can be reordered arbitrary amongst themselves.
fn foo(d: Box<dyn AssocAndParams<String, usize, Assoc1 = i32, Assoc2 = u32>>)
->
    Box<dyn AssocAndParams<String, usize, Assoc2 = u32, Assoc1 = i32>>
{
   d
}
}

Opting out of dyn-usability

As of Rust 1.72, if you add a where Self: Sized bound to an associated type, it is considered non-dyn-usable. The associated type becomes unusable by dyn Trait, and you no longer need to constrain the associated type with the named parameter.

#![allow(unused)]
fn main() {
trait Trait {
    type Foo where Self: Sized;
    fn foo(&self) -> Self::Foo where Self: Sized;
    fn bar(&self) {}
}

impl Trait for i32 {
    type Foo = ();
    fn foo(&self) -> Self::Foo {}
}

impl Trait for u64 {
    type Foo = f32;
    fn foo(&self) -> Self::Foo { 0.0 }
}

// No need for `dyn Trait<Foo = ()>`!
let mut a: &dyn Trait = &0_i32;

// No need for associated type equality between base types!
a = &0_u64;

// This fails because the type is not defined (`dyn Trait` is not `Sized`)
// let _: <dyn Trait as Trait>::Foo = todo!();
}

Although it produces is awarning, you can still optionally specify the associated type, even though it's not usable by the dyn Trait itself. Note also that this does result in incompatible types and limits the possible coercions:

#![allow(unused)]
fn main() {
trait Trait {
   type Foo where Self: Sized;
   fn foo(&self) -> Self::Foo where Self: Sized;
   fn bar(&self) {}
}
impl Trait for i32 {
   type Foo = ();
   fn foo(&self) -> Self::Foo {}
}
impl Trait for u64 {
   type Foo = f32;
   fn foo(&self) -> Self::Foo { 0.0 }
}
let mut a: &dyn Trait<Foo = ()> = &0_i32;

// Fails!
a = &0_u64;
}

This introduces some interesting possibilities around implementing trait for Box<dyn Trait>:

#![allow(unused)]
fn main() {
trait Trait {
   type Foo where Self: Sized;
   fn foo(&self) -> Self::Foo where Self: Sized;
   fn bar(&self) {}
}
impl Trait for i32 {
   type Foo = ();
   fn foo(&self) -> Self::Foo {}
}
impl Trait for u64 {
   type Foo = f32;
   fn foo(&self) -> Self::Foo { 0.0 }
}
impl<T: Default> Trait for Box<dyn Trait<Foo = T>> {
    type Foo = T;
    fn foo(&self) -> Self::Foo {
        T::default()
    }
}
}

The warning currently says "while the associated type can be specified, it cannot be used in any way," but this example shows that is not technically true. I think this sort of usage was just not anticipated.

The reason it's not an error to specify non-dyn-usable associated types in this manner is that there was a period where you could add Self: Sized bounds to associated types, but were still required to name the associated type in dyn Trait<..>. Thus it would be a breaking change to make the warning an error.

Given the potential utility, I would argue that the warning should at a minimum be reworded, and perhaps renamed.

No nested coercions

An unsizing coercion needs to happen behind a layer of indirection (such as a reference or in a Box) in order to accomodate the wide pointer to the erased type's vtable (and because moving unsized types is not supported).

However, the unsizing coercion can only happen behind a single layer of indirection. For example, you can't coerce a Vec<Box<T>> to a Vec<Box<dyn Trait>>. Why not? Box<T> and Box<dyn Trait> have different layouts! The former is the size of one pointer, while the second is the size of two pointers. The entire Vec would need to be reallocated to accomodate such a change:

#![allow(unused)]
fn main() {
trait Trait {}
fn convert_vec<'a, T: Trait + 'a>(v: Vec<Box<T>>) -> Vec<Box<dyn Trait + 'a>> {
    v.into_iter().map(|bx| bx as _).collect()
}
}

In general, unsizing coercions consume the original pointer (reference, Box, etc) and produce a new one, and this cannot happen in a nested context.

Internally, which coercions are possible are determined by the CoerceUnsized trait, and the (compiler-implemented) Unsize trait, as discussed in the documentation.

Except when you can

There are some material and some apparent exceptions where unsizing coercion can occur in a nested context.

If you follow the link above, you'll see that some types such as Cell implement CoerceUnsized in a recursive manner. The idea is that Cell and the others have the same layout as their generic type parameter. As a result, outer layers of Cell don't count as "nesting".

#![allow(unused)]
fn main() {
use std::cell::Cell;
trait Trait {}
// Fails :-(
//fn coerce_vec<'a, T: Trait + 'a>(v: Vec<Box<T>>) -> Vec<Box<dyn Trait + 'a>> {
//    v
//}

// Works! :-)
fn coerce_cell<'a, T: Trait + 'a>(c: Cell<Box<T>>) -> Cell<Box<dyn Trait + 'a>> {
    c
}
}

We'll cover the apparent exceptions (which are actually just supertype coercions) in an upcoming section.

The Sized limitation

Base types must meet a Sized bound in order to be able to be coerced to dyn Trait. For example, &str cannot be coerced to &dyn Display even though str implements Display, because str is unsized.

Why is this limitation in place? &str is also a wide pointer; it consists of a pointer to the UTF8 bytes, and a usize which is the number of bytes. Similarly a slice reference &[T] is a pointer to the contiguous data, and a count of the number of items.

A &dyn Trait created from a &str or &[T] would thus naively need to be a "super-wide pointer", with a pointer to the data, the element count, and the vtable pointer. But &dyn Trait is a concrete type with a static layout -- two pointers -- so this naive approach can't work. Moreover, what if I wanted to coerce a super-wide pointer? Each recursive coercion requires another pointer, making the size unbounded.

A non-naive approach would require special-casing how dynamic dispatch works for erased non-Sized base types. For example, once you've type erased str, you've lost the information that &str is also a wide pointer, and how to create that wide pointer. However, the code would need to recreate a wide pointer in order to perform dynamic dispatch.

So for dyn Trait to non-naively support unsized types, it would need to examine at run-time how to construct a pointer to the erased base type: one possibility for thin pointers, and an additional possibility for each type of wide pointer supported. Not only that, but the metadata required (such as the length of the str) has to be stored somewhere, and that can't be in static memory like the vtable is.

Instead, unsized base types are simply not supported.

Sometimes you can work around the limitation by, for example, implementing the trait for &str instead of str, and then coercing a &'_ str to dyn Trait + '_ (since references are always Sized).

#![allow(unused)]
fn main() {
use std::fmt::Display;
// This fails as we cannot coerce `str` to `dyn Display`, so we cannot coerce
// `&str` to `&dyn Display`.
// let _: &dyn Display = "hi";

// However, `&str` also implements `Display`.  (If `T: Display`, then `&T: Display`.)
// Because `&str` is `Sized`, we can instead coerce `&&str` to `&dyn Display`:
let _: &dyn Display = &"hi";
}

Sized is also used as a sort of "not-dyn" marker, which we explore later.

There is one broad exception to the Sized limitation: coercing between forms of dyn Trait itself, which we look at immediately below.

Discarding auto traits

You can coerce a dyn Trait + Send to a dyn Trait, and similarly discard any other auto trait.

Although dyn Trait isn't a supertype of dyn Trait + Send, this is nonetheless referred to as upcasting dyn Trait + Send to dyn Trait.

Note that auto traits have no methods, and thus no change to the vtable is required for these coercions. They allow one to call a less restricted function (that takes dyn Trait) from a more restrictive one (e.g. one that requires dyn Trait + Send). The coercion is necessary as, again, these are (distinct) concrete types, and not generics nor subtypes nor dynamic types.

Although no change to the vtable is required, this coercion can still not happen in a nested context.

The reflexive case

You can cast dyn Trait to dyn Trait.

Sorry, we're being too imprecise again. You can cast a dyn Trait + 'a to a dyn Trait + 'b, where 'a: 'b. This is important for how borrowing works with dyn Trait + '_.

As lifetimes are erased during compilation, the vtable is the same regardless of the lifetime. Despite that, this unsizing coercion can still not happen in a nested context.

However, in a future section we'll see how variance can allow shortening the trait object lifetime even in nested context, provided that context is also covariant. The section after that about higher-ranked types explores another lifetime-related coercion which could also be considered reflexive.

Supertrait upcasting

Though not supported on stable yet, the ability to upcast from dyn SubTrait to dyn SuperTrait is a feature expected to be available some day.

It is, once again, explicitly a coercion and not a sub/super type relationship (despite the terminology). Although this is an implementation detail, the conversion will probably involve replacing the vtable pointer (in contrast with the last couple of examples).

Until the feature is stable, you can write your own "manual" supertrait upcasts.

Object-safe traits only

There are other restrictions on the trait which we have not discussed here, such as not (yet) supporting traits with generic associated types (GATs). We cover those in the next section.