4

I'm trying to create a map, where a key is a TypeId of a trait and a value is a struct that implements it.

impls: HashMap<TypeId, Rc<dyn Any>>

To put a new object to the map, I use the next code:

let type_id = TypeId::of::<dyn Trait>();
let obj_ref: Rc<dyn Any> = Rc::new(obj);

self.impls.insert(type_id, Rc::clone(&obj_ref));

But when I try to get this object from the map, I need to cast it back.

let any_comp_ref = self.impls
    .get(&TypeId::of::<dyn Trait>())
    .expect(&format!("Object for type: {} is not found", type_name::<dyn Trait>()));

let result: &dyn Trait = // ???

I've tried to use Rc::downcast. But it's implemented only for Sized types.

Peter Hall
  • 53,120
  • 14
  • 139
  • 204
Uraty
  • 257
  • 2
  • 7
  • If you are willing to go unsafe, you could save them as `std::raw::TraitObject` values or an equivalent non-nightly type. Highly unsafe and tricky to do right, but possible. – rodrigo Aug 15 '20 at 15:20
  • I actually think this is an oversight in the standard library. Maybe it's worth opening an issue? I think that `Any::is`, as well as all of the `downcast_*` methods in the standard library, should probably accept unsized types. – Coder-256 Aug 16 '20 at 04:59
  • 1
    @rodrigo `std::raw::TraitObject` does not have the same layout as an `Rc`, since it also has a pointer to the ref counts. – Peter Hall Aug 16 '20 at 05:06
  • @Coder-256 It's not an oversight, it's a logical consequence of the lack of RTTI in Rust. `dyn Any` doesn't retain enough information about the concrete type to "know" whether it implements any arbitrary trait, so such a method would have to materialize a vtable-pointer out of thin air. – trent Aug 17 '20 at 13:43
  • 1
    Does this answer your question? [How can I downcast from Box to a trait object type?](https://stackoverflow.com/questions/25246443/how-can-i-downcast-from-boxany-to-a-trait-object-type) Relevant excerpt: "you can only have one level of traityness[...] if you take a type `Widget` that implements `WidgetTrait` and box it in a `Box`, you can only get it out as a `Widget` object, not as a `Box` object." – trent Aug 17 '20 at 15:09
  • If that's not it, perhaps you can solve your problem by [double-boxing](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e02151fa81b62cda93230b5514587a82)? – trent Aug 17 '20 at 15:26
  • @trentcl I am aware; `dyn Any` stores information for exactly one type (such as a trait, struct, enum, etc.). My only point was that I don't see why the implicit `Sized` bound is necessary for so many of those methods, and at least `Any::type_id` already doesn't require `Sized`. – Coder-256 Aug 17 '20 at 17:05
  • @Coder-256 `downcast_ref` does need `Sized` though, because there's no way to go from (data ptr + `Any`-vtable ptr) to (data ptr + arbitrary metadata) because `Any` vtables don't retain any of that metadata that would be needed to downcast to an arbitrary trait object or slice type. (Not to mention that you can't cast `&dyn Trait` to `&dyn Any` in the first place; see [Why can't `&(?Sized + Trait)` be cast to `&dyn Trait`?](/q/57398118/3650362)) – trent Aug 17 '20 at 18:01
  • @trentcl Ok I see. Still, this seems like a compiler limitation, unless there is a possible blowup in pointer width? (I can’t seem to think of a situation where that would happen). In other words why can’t there be 3-pointer fat pointers? – Coder-256 Aug 17 '20 at 22:46
  • @Coder-256 Well, if you do make an x3 "obese pointer", it can't be `Box`, which is only two pointers. Let's call it `Box` to indicate we don't know what the second trait is. You *still* can't write `downcast_obese` from `Box` to `Box` because traits don't have a `TypeId` equivalent to check what *kind* of vtable it is. So the best you can do is `Box`, where `Trait` is known, but that doesn't solve OP's problem because they want to have multiple trait object types in a collection. – trent Aug 17 '20 at 23:06
  • Obese pointers (this is a term of art, BTW, not an original idea) are possible, they've been suggested before, they probably won't be added to the language, but even if they were they don't solve this problem. If you added a `TraitId` language mechanic as well, you *could*, but -- unlike `TypeId` -- `TraitId` is not zero-cost, because every vtable effectively has to have a runtime `TraitId` *whether or not* you ever try to `downcast_obese` it. – trent Aug 17 '20 at 23:10
  • @trentcl That's a very good point. Honestly I don't think an extra `u64` for the `TraitId` in each vtable is very much of a cost, but that plus obese pointers plus multiple vtables for each trait certainly complicate things to the point where it's nowhere near trivial to implement, and it definitely seems like too much for something so niche. – Coder-256 Aug 18 '20 at 01:49

0 Answers0