Regarding the inability to use `dyn Trait` methods via `&dyn Trait + Send`

dyn Trait is not a supertype of dyn Trait + Send, and we cannot call dyn Trait's methods via a &dyn Trait + Send without any explicit type coercion. There is an awesome blog post covering this.

A consequence is that sometimes, we must write boilerplate code that implements something for dyn Trait, dyn Trait + Send, and dyn Trait + Send + Sync (or maybe more?) separately. std::any::Any and std::error::Error are perfect examples.

My question is: why does dyn Trait work like this? Will something bad happen if we can call dyn Trait's methods via &dyn Trait + Send?
Thanks in advance.

3 Likes

Can you elaborate a bit on what you mean exactly with this? I assume you referring to this paragraph of @quinedot's article:

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.

But that refers to implementing some trait for &dyn Trait or (some other type involving dyn Trait). I'm confused by what you mean by "calling dyn Trait's methods." For me that sounds like Trait has a method foo and you have an instance of &Trait + Send and you want to call foo on that instance. This is perfectly fine without any coercion or concerns about something bad happening.

2 Likes

I mean methods implemented on dyn Trait, like this playground or like what std::error::Error does.

2 Likes

Ah I see, so you have something like:

impl dyn Foo + '_ {
    fn method_on_foo(&self) {
        println!("method on Foo");
    }
}

and you can't call method_on_foo on &dyn Foo + Send but you can on &dyn Foo and you want to coerce the former to the latter in order to not repeat the implementation? Like:

fn use_foo(f: &(dyn Foo + Send)) {
    (f as &dyn Foo).method_on_foo();
}
4 Likes

Yes, that's exactly what I intended to ask in this question. Thanks for the detailed reply!

I will try to make myself clearer: I guess it is OK in many cases to omit the explicit coercion (so we can just write f.method_on_foo()) since AFAIK the vtable is the same with or without the Send bound. However, currently there should be no rules requiring the compiler to look at dyn Trait's methods when the method receiver is a &dyn Trait + Send. Usually when Rust prevents me from doing something, it's because doing so will result in something bad. Is this dyn Trait constraint also the case?

I wouldn't think of this as a constraint, more like dyn Trait and dyn Trait + Send are distinct types. If you think like this it is not confusing that you can't call dyn Trait's methods on dyn Trait + Send. It'd be the same as expecting calling struct Foo's methods on struct Bar to work.

However—and that is the part where I'm not quite understanding what is going on—you can cast dyn Trait + Send to dyn Trait. I mean that you can cast it makes sense to me. As you said, the vtable should be the same, with or without Send. But I'm not sure which casting or coercion type allows the casting in your case [1].


  1. If I had to guess, I'd expect that there is some rule applying to auto-traits or unsized coercion, but I don't know. ↩︎

To me, these facts both make sense, though if we put them together, things can get a little counter-intuitive:

  • dyn Trait + Send and dyn Trait are distinct types;
  • we can coerce the former to the latter.

IMO the key point here is the coercion. Consider u32 and u64: they are distinct types, have different methods, and can be casted to each other, but you cannot pass a u32 as an argument to a fn(usize) -> () unless you cast it with as u64. Meanwhile, it is totally fine to pass a &dyn Trait + Send to a fn(&dyn Trait) -> (), but when that parameter is &self, the code will not compile.

Anyway, the only use case I can imagine of impl dyn Trait is downcasting, which is not very common. So probably the effort to explicitly cast &dyn Trait + Send to &dyn Trait to use dyn Trait's methods only occur in rare cases. But it still would be good to know the mechanisms of these types and the underlying reasons, if there are any.

Nice solution!

The cast is pretty reasonable and should be encouraged.

There is even an idea to warn against &(dyn … + Send) as a rustc lint (instead of a clippy one).

Adding + Send to a &dyn is a useless restriction. E.g. &(dyn Any + Send) implicitly converts to &dyn Any , but not the other way around, even though it'd be sound to just transmute it in both directions, since the Send isn't of any use for (shared) references.

src: Add a lint to warn about `&(dyn … + Send)` · Issue #110937 · rust-lang/rust · GitHub

The Send in &(dyn Any + Send) is basically useless, because it's a reference. Removing it from PanicInfo::payload() allows for some new possibilities in the future, if the type behind it no longer needs to be Send . (We could just safely transmute the + Send into the &dyn because it's meaningless for references, but it'd be nicer to just remove the needless + Send from the public API to clean things up.

src: Use &dyn Any rather than &(dyn Any + Send) for PanicInfo::payload() by m-ou-se · Pull Request #110799 · rust-lang/rust · GitHub

3 Likes

Send is an example, and the same holds true for &(dyn Trait + Send + Sync) and Box<dyn Trait + Send>.

I must say that I am unaware that the Send bound in &(dyn Trait + Send) is useless until I see this post, but this is still another topic.

The fact that &T is only Send if T: Sync makes the Send marker on T do nothing to make &T implement Send as well. For Box it would be different, because Box is only Send when T is Send, too.

1 Like

It's Unsize I believe. It has to be magical because it applies to every trait, and Rust doesn't have generic trait parameters.

An argument against auto-coercing these cases is that you'd need to define an order. If dyn Trait + Send + Sync doesn't have a method, but dyn Trait + Send and dyn Trait + Sync does, which wins? How about when I have dyn Trait + Send + Sync + SomeFutureAutoTrait? What about custom auto traits? What if we ever allow dyn Trait + NonAutoTrait? Etc.

6 Likes

Additionally dyn Trait + … can implement Deref

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.