A tour of `dyn Trait`

dyn Trait comes up a lot in this forum in various ways:

  • not understanding the fundamentals of dyn Trait, e.g. confusion with generics or subtyping
  • more typical lifetime problems due to dyn Trait's object lifetime and its elision rules
  • achieving practical goals like being able to clone a Box<dyn Trait>
  • and more

So I decided to create a guide on the topic for Rust learners:

A tour of dyn Trait

Rust's type-erasing dyn Trait offers a way to treat different implementors of a trait in a homogenous fashion while remaining strictly and statically (i.e. compile-time) typed. For example: if you want a Vec of values which implement your trait, but they might not all be the same base type, you need type erasure so that you can create a Vec<Box<dyn Trait>> or similar.

dyn Trait is also useful in some situations where generics are undesirable, or to type erase unnameable types such as closures into something you need to name (such as a field type, an associated type, or a trait method return type).

There is a lot to know about when and how dyn Trait works or does not, and how this ties together with generics, lifetimes, and Rust's type system more generally. It is therefore not uncommon to get somewhat confused about dyn Trait when learning Rust.

In this section we take a look at what dyn Trait is and is not, the limitations around using it, how it relates to generics and opaque types, and more.

continue on to the overview...

dyn Trait has a lot of special functionality and corner-cases, so this ended up being quite long. The guide is really also part reference material (due to the high number of corner-cases covered) and part cookbook (as some parts of working with dyn Trait are just best illustrated by example).

Feedback is welcome here or on GitHub (though I'm more attentive here).

I hope you enjoy it!

28 Likes

On "the implementation cannot be directly overrode", you could mention https://github.com/rust-lang/rust/issues/57893 if you want. (Also wouldn't "overridden" be more grammatical?)

4 Likes

Funny thing, I just stumbled across that issue earlier today! [1][2]


  1. Probably good it happened after I posted this or it might have been delayed another week :sweat_smile: ↩︎

  2. Pretty sure I'd seen it some time in the past but lost track ↩︎

1 Like

Rule of thumb: “if it smells like specialization, itʼs probably unsound”.

4 Likes

Re dyn Trait coercion. There are actually two types of coercions: The ones that only happen in CoerceUnsized positions, and this not behind any double-indirection, and the ones that are also true subtyping and supported behind deep nesting, given the right variance.

dyn Trait + 'a to dyn Trait + 'b coercion is of the second type, so that it works for

  • types like Vec<Box<dyn Trait + '_>> because of covariance, but also
  • &mut (dyn Trait + '_) because that position supports unsized coercions, and despite the invariance of the type parameter of &mut T.

This means your remark on "the reflective case" that "this coercion can still not happen in a nested context" is wrong.

The same applies to the dyn for<'a> Trait<'a> to dyn Trait<'b> coercion that you don't even mention at all (or I didn't see it).

This is an interesting example of subtyping between two types T: 'static – like Box<dyn for<'a> Trait<'a>> to Box<dyn Trait<'static>> – which demonstrates that T: 'static is insufficient to make sure you don't need to think about variance (which may be relevant for designing unsafe code; e. g. these two types will not have the same TypeId, so never cache the TypeId of a covariant parameter even it it's bound by 'static).

3 Likes

You mean it's both basically, right? The first can happen in places of invariance, but not nested; the second can happen nested, but not in places of invariance. Anyway, I should have an example pointing this out. ...Probably you haven't reached a later section about variance yet, I'll think about how to make the earlier section less counterfactual or counterfactual-seeming.

I don't really cover higher-ranked types at all,[1] or Fn and friends (a very common target for dyn). It would be another sizable addition -- which may happen, but if it does, it will probably be a new section about higher-ranked types generally, function types and fn pointers, closure types, and the Fn traits.

Probably I should figure out a place in this section to at least briefly go over higher-ranked dyn Trait types.


  1. a brief fn pointer example in the dyn Any section ↩︎

3 Likes

I see. Indeed I didn't read the later chapter yet. I thing a forward link might be enough. Something like "Despite that, this coercion can still not happen in a nested context, however as we discuss later variance rules can still allow for similar coercions." (with appropriate links added).

Also, is this heading supposed to say "reflexive"?

1 Like

Upon reflection... yes.

2 Likes

Advanced guidelines - Learning Rust

If your type has no lifetime parameter, or if there is no bound between the type parameter and the lifetime parameter, the default for elided dyn Trait lifetimes will be 'static, like it is for Box<T>. This is true even if there is an implied T: 'a bound.

That last sentence is an impressive observation and something I didn't know. Do you know if this is a deliberate design decision?

1 Like

Deliberate.

2 Likes

At a guess, it's there so that implementation details of private fields don't leak into the public interface.

1 Like

This is great!

I'm wondering if saying that trait object types are "statically known" might be confusing.

The compiler knows about the concrete type at compile time to assure that it does implement the trait, but saying it's statically known seems confusing to me since it will be looking up the concrete type in the vtable at runtime, right? I'd think the reason it allows you to store trait objects of different concrete types in the same Vec is that the concrete type isn't statically known (in that code location).

Some feedback on the formatting (probably just bugs) (Chrome, macOS and Android):

  • The button to show/hide the sidebar doesn't do anything (which means if you load the page on a narrow screen you can't get to it).
  • All links are appearing as plain text except on hover.
  • The header is transparent and the body text appears underneath it.

1 Like

Same for me. On my phone, the website only displays properly on Firefox (among the two browsers I tested), while other mdbook-generated pages work fine.

Yeah. I tried to fix those errors, but didn't find the chrome.css file in the repo :sweat_smile:

I think the point of that section is that, from the perspective of the type system, dyn Trait is a single concrete type— There's plenty of compiler magic involved to create one, but once you have one it behaves exactly the same as any other !Sized type.

2 Likes

If the type system believed it was a single concrete type, wouldn't we be able to call non-trait methods on the type? It seems very confusing to word it like this since trait objects are a form of polymorphism.

You can define and call inherent methods on dyn Trait just fine, with the only caveat being that they can't have the same name as the trait's methods. The most common example of this is probably these inherent methods of dyn Any

5 Likes

The point is, like @2e71828 said, that dyn Trait + 'a is a distinct, concrete, statically known type and not a dynamic type. The erased base type isn't statically known.

I'll see if I can fit in an example of implementing an inherant method on a dyn Trait and some other forward links like implementing other traits on dyn Trait. If you have other ideas on how I can make the distinction clear, please let me know!

1 Like

Huh, weird, I'm just uploading mdbook output. I wonder if there's some accidental exclusion going on or what. The chrome.css file was included.

Anyway thanks for letting me know, I'll poke around and see what I can figure out.