[Idea] Dynamic dispatch for generic structs

Say I have a generic struct MyStruct<T> and I want to generalize over T for some reason. Currently, there is only one way to do this: I have to create a trait MyStructTrait and then change the struct's implementation from impl <T> MyStruct<T> to impl <T> MyStructTrait for MyStruct<T>. I then have wrapped my struct in a trait object and can do dynamic dispatch. But this is un-ergonomic and has several downsides:

  • Duplication of all methods between MyStructTrait and MyStruct
  • Naming problems, since the struct and that trait are go together 1:1
  • That's not the way traits are intended to be used
  • Outside accesses to fields are not possible anymore, I'll have to create getters (twice, see above)

What I think of is that it would be nice to create a type like dyn MyStruct, where the type parameter T is erased and replaced by dynamic dispatch instead. As with trait objects, "struct objects" will have a set of side-conditions to make them "object-safe". For example, any methods or fields exposing T cannot be accessed through a struct object.

1 Like

What does this mean exactly? In the abstract I don't see how this leads specifically to the single-use trait you're describing.

Could you describe a concrete use case or two?

Basically, I want to erase T for outside users so I don't have to propagate it through the whole call hierarchy.

The main use case I have is a Vec<MyStruct<T>>, where each item may have a different value for T: Vec<dyn MyStruct>.

Sometimes, I don't want monomorphization to happen. The generic type might be an implementation detail that may change at any time, so I don't want to expose it in the method signature. Sometimes the code size overhead of monomorphization is not worth the level of indirection.

1 Like

Is that really going to be just any kind of type T? Because then you couldn’t do much with it. How do you recover from an unknown type? Or is it a finite list of them? Then some sort of enum would be useful, probably. Or are they all T: SomeTrait. Then maybe MyStruct<dyn SomeTrait> might work (depending on what kind of data structure the MyStruct type is).

Or is it only a single type? Then wrap it to hide it, struct MyStructVisible(MyStruct<HiddenType>).

The “use case” you describe could be a bit more concrete ^^

1 Like

Yeah, as mentioned this already works for some limited cases, more general cases would likely not be able to be supported even with a different syntax, it's a fundamental limitation of how trait objects are implemented (playground)

use core::fmt::Debug;

#[derive(Debug)]
struct MyStruct<T: ?Sized> {
    foo: usize,
    bar: T,
}

fn main() {
    let vals: Vec<Box<MyStruct<dyn Debug>>> = vec![
        Box::new(MyStruct { foo: 5, bar: 6 }),
        Box::new(MyStruct { foo: 7, bar: "hello" }),
    ];
    dbg!(vals);
}

Is that really going to be just any kind of type T ?

Maybe. Putting T under some type bounds never hurts. But even no outer knowledge about T doesn't make it useless: However dyn MyStruct is implemented, there will always be a concrete T and a concrete implementation present when that object is created. It's just that an outside holder of the object can't know it anymore, because the details are hidden behind a layer of indirection.

@Nemo157 Your solution is really interesting. I already played around with this and I got stuck by the size requirement. That forced my to us MyStruct<Box<dyn Debug>> which I didn't really like. In your solution, where does the dynamic dispatch happen? How does it work, how does it get monomorphized?

@steffahn I have an Effect trait with different implementations. They are all wrapped by some struct EffectContainer<E: Effect> of which I now want to hold a Vec with different internal effect implementations. The EffectContainer needs to know about its concrete E, but the holder of the Vec shouldn't care about that anymore.

Okay, the Box<EffectContainer<dyn Effect>> approach only works if:

  • EffectContainer<E> has E as an ?Sized parameter, only used in its last field.. the name of this feature is dynamically sized types.
  • All methods of (or functions taking) EffectContainer that you need when accessing the container from the Vec can handle E and the container being of unknown size. (Methods for constructing the container don’t need to; after construction you’d use unsized coercion on the Box before inserting into the Vec.)

The dynamic dispatch would be on the level of methods of Effect. Unlike your original approach of creating an extra trait for the EffectContainer methods. In that approach you’d get all the trait’s methods in all their monomorphized variants precompiled and optimized and probably fewer points of dynamic dispatch.

If using the unsized approach doesn’t work then after all, your original idea might be the best approach. You can also probably eliminate some of the shortcomings:

You would probably not need the methods on both. You could keep methods for construction on the struct and implement other methods only in the trait. The only duplication is between the trait definition and implementation. You could solve problems with private fields of the struct by using things like pub(self) or pub(crate) on the fields or by making the whole struct private, then with public fields, and only exposing the trait.

Maybe you could elaborate on those.

Since dynamic dispatch in rust only happens with traits, I guess it is something that traits are intended to be used for.

The problem with field access probably remains.

The only duplication is between the trait definition and implementation.

That's the exactly duplication I was referring to, since I've already done most of what you suggest.

Naming problems

So the naming problem arises from the fact that I need two names for essentially one thing: One for the trait and one for the corresponding struct. I've tried EffectContainer+EffectContainerImpl and DynEffectContainer+EffectContainer, but those aren't really great names.

I tried to implement @Nemo157's solution and I ran into exactly the limitations you just listed. The main problem is that if T: ?Sized, I cannot pass it as argument within the trait any more.

Maybe the whole thing boils down to this: I want to move the dynamic dispatch from Box<MyStruct<dyn Debug>> to an outer layer Box<dyn MyStruct<Debug>>.

If there is no idiomatic way to implement this in Rust, could I instead implement this using macros? This wouldn't make the solution any better, but at least it could hide the boilerplate from the developer.

I'm not sure, but perhaps there already is some crate with macros that can generate from a struct impl block a corresponding trait and implementation. Definitely possible with procedural macros and for the user only a single attribute on the block.

I'm still very unclear what's being asked for here, but the problem of traits and their impl blocks having to repeat all of the same function signatures is usually discussed under a term like "delegation", and https://crates.io/crates/ambassador implements quite a bit of delegation with macros alone. So maybe this is just another case of that.

1 Like

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