What's the difference between T: Trait and dyn Trait?

I always thought they were the same, but this code implements a trait for both of them:

2 Likes

I admit I have never seen this, nor used it, but as far as I can tell:

impl AsDynError for dyn Error + 'static

Implements the trait for trait objects who's type is unknown at compile time, like &dyn Error or Box<dyn Error>, whereas T: Error is generic, but it's type is known at compile time.

I just never thought of implementing a trait for trait objects, and I didn't know it was possible.

2 Likes
  • <T : Trait> means that the function / trait / etc. can be "fed" / "instanced with", at compile time, any type T as long as T implements Trait. For each different T, a different version of the code will be generated / copy-pasted.

  • dyn Trait, on the other hand, represents only one single concrete type, the one that unifies "all" the (other) T : Trait. Indeed, any (slim) pointer to a T when T : Trait can be coerced into a (fat) pointer to the type dyn Trait. And the type dyn Trait has an automagically compiler-generated impl Trait for dyn Trait { ... }. This way, instead of having to deal with each and every different T : Trait, we just have to deal with this one type.

    • For the coercion to work, T needs to be Sized, which is always the case with a generic type parameter unless we "remove" the : Sized implicit bound with the ?Sized "unbound".

    • If struct Foo : Trait, enum Bar : Trait and union Baz : Trait, where Foo, Bar and Baz are fixed-size types, then <T : Trait> is a genericity that can use any of these types, which can be coerced to dynamically sized dyn Trait (i.e., dyn Trait : !Sized).

    • It is thus possible to also include dyn Trait generically, with the ?Sized unbound:
      <T : ?Sized + Trait>

To better illustrate this, let's draw a parallel with slice vs. array:

  • the type T, where <T : Trait> is similar to the type [u8; N], where <const N: usize>: for every concrete length N = 0, ..., 42, we get to copy-paste the "template" code using the concrete type [u8; 0], [u8; 1], ..., etc. [u8; 42], and this way, the length of the array is fixed, each time.
    We get the advantage of being able to stack-allocate and/or inline things by taking advantage of the kownledge of the fixed length / size.
    But we don't get to be able to feed it an array whose length is unknown at compile time: a slice, of type [u8].
    So in some cases we'd rather have a function work on [u8] rather than making it generic over <const N: usize> [u8; N]. And this is okay, because as long as we have a pointer to a [u8; N] (e.g., &[u8; N], &mut [u8; N], Box<[u8; N]>, Arc<[u8; N]>, etc.), we can coerce such (slim) pointer into a (fat) pointer to a [u8], whereby the pointer has been enlarged with some necessary runtime metadata (in this case, the length n of the slice).

  • Well then this is the same with <T : Trait>: in some cases we'd rather have a function work on dyn Trait rather than making it generic over <T : Trait>. And this is okay, because as long as we have a pointer to a T (e.g., &T, &mut T, Box<T>, Arc<T>, etc.), we can coerce such (slim) pointer into a (fat) pointer to a dyn Trait, whereby the pointer has been enlarged with some necessary runtime metadata (in this case, a pointer to a a struct containing the size, alignment, destructor, and a bunch of function pointers corresponding to the methods defined in impl Trait for T { ... }).

20 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.