Rust return dyn trait object which has generic type parameters

Context:

  • Store is the base trait for any-store ops
  • KeyTrait is for any key sent to Store.
  • AStore and BStore are two variant of store implementations, users can define their own Store impl
pub trait Store {
    fn put(&self, item: impl KeyTrait);
}

Error:

  • can not build a function to return a dynamic resolved store impl due to Store cannot be made into an object

Goal:

  • provide user a way to create store dynamically, user can define their own Store impl and pass in, and the store instance should be dynamically determined during runtime.
  • ideally with dyn trait instead of trait bound
pub fn get_store(store_type: &str) -> Box<dyn Store> { # <-- error
    match store_type {
        "A" => Box::new(AStore{}),
        _ => Box::new(BStore{}),
    };
}
fn main() {
    run_with_store(Arc::new(get_store("A")));
}

===

The current solution we have is

fn main() {
    let store_type = "A";
    match store_type {
        "A" => run_with_store(Arc::new(AStore{})),
        _ => panic!(""),
    };
}

we dont want this as this will repeat run_with_store with different object variants, and our system will create lot of variant combos.

e.g. run_with_store(A, a); run_with_store(A, b); run_with_store(B, a) etc

I don't know if I entirely understand your goals, but instead of

    fn put(&self, item: impl KeyTrait);

you could use

    fn put(&self, item: Box<dyn KeyTrait>);
    // Or `&dyn KeyTrait` or whatever makes the most sense

in order to make the trait object-safe.

See also, and I also implemented Store for Box<dyn Store + '_> to make the playground compile (mechanically; I didn't try to see if there was a better solution).

Thank you,

I get the idea of putting the trait into a box to make it thread safe, another question:

if we were to stick with generic type param instead of boxing it for some reason, the problem is I am little uncomfortable with the way it instantiated.

fn run_with(s: S, q: Q)
where S: Store, Q: Queue {
}

case 1: run_with(AStore{}, AQueue{}); 
case 2: run_with(AStore{}, BQueue{}); 
case 3: run_with(BStore{}, AQueue{});
case 4: run_with(BStore{}, BQueue{});

Is there any elegant way to do so like trait object

fn run_with(s: dyn Store, q: dyn Queue) {}
fn get_store() -> impl Store;
fn get_queue() -> impl Queue;
run_with(get_store(), get_queue())

You can define a declarative macro that encloses object values into Box::new(...)

See Macros by Example

A borrow is simpler though, as quinedot also says...

fn f(a: &dyn YourTrait) {
}
f(&Impl1 { ... });

dyn Trait isn't about thread safety, it's about erasing different base types into a common, dynamically dispatched type. Indeed, if thread safety is a concern, you probably want dyn Trait + Send + Sync or the like.

dyn Trait is unsized and thus typically behind a pointer, in particular it has to be behind a pointer to pass by value. So some ergonomic hit to the example is required if you change from generics to some form of dyn Trait. You have to do the coercion to dyn Trait behind some sort of pointer.

For the contrary situation, if you implement Trait for &dyn Trait, Trait for Box<dyn Trait> and so on, then you can pass them to functions with the generic bound.

1 Like

When passing a parameter for a type that implements YourTrait, you can make the parameter's type &dyn YourTrait as others have said. In this case the instance of the type (that implements the trait) does not have to be Boxed separately. It may be in a local variable on the stack, in a Vec containing objects of that type, etc.

But a function that returns an impl Trait must always returns the same type. It can't return different types, depending on the parameter as you're trying to do. (Note that a trait is not a type, its a bound for a type.)

That's why, to return different types from a single function you need to Box the object when returning it. This allows you to return a dyn pointer, which means the function return value will be Box<dyn YourTrait>, not impl YourTrait. This forces you to Box each object separately, meaning that each object is allocated separately. One reason for this restriction is that different types that implement a common trait may have different sizes.

However, there is a great way to work around this and avoid the Boxing/allocation, while at the same time improving performance: use the enum_dispatch crate. This only works when there is a known set of types that implement a given trait, but often that is the case. See the example at the link. You create an enum containing the different types that implement a specific trait, and the #[enum_dispatch] macro does the rest of the work. The performance gain comes from two things: avoiding allocation, and not having to call through a dispatch table like dyn uses. This crate is extremely popular and there are no drawbacks to using it, other than the restriction I mentioned about having a fixed set of types, that are known at compile time, that implement the trait.


Here's a quick example where the Store trait types are converted to use enum_dispatch. I didn't convert the KeyTrait types. I removed all the boxing.

The important thing to notice is that the enum_dispatch macro has implemented the Store trait for the StoreEnum type, so you can pass a StoreEnum for a parameter with a Store trait bound. The generated implementation efficiently forwards the trait method call to the variant of the enum that is used. This forwarding is faster than calling via a &dyn reference.

use enum_dispatch::enum_dispatch;

// Define the itemTrait
pub trait KeyTrait {}

#[enum_dispatch(StoreEnum)]
pub trait Store {
    // Modify the put method signature to include an item of itemTrait
    fn put(&self, item: &dyn KeyTrait);
}

#[enum_dispatch]
enum StoreEnum {
    AStore,
    BStore,
}

pub struct AStore {}

impl Store for AStore {
    fn put(&self, item: &dyn KeyTrait) {
        println!("A put");
    }
}

pub struct BStore {}

impl Store for BStore {
    fn put(&self, _item: &dyn KeyTrait) {
        println!("B put");
    }
}

pub struct FooKey {}

impl KeyTrait for FooKey {}

pub fn run_with_store<T>(s: T)
where
    T: Store,
{
    s.put(&FooKey {});
}

pub fn get_store(store_type: &str) -> StoreEnum {
    match store_type {
        "A" => AStore {}.into(),
        _ => BStore {}.into(),
    }
}
fn main() {
    run_with_store(get_store("A"));
}
1 Like

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.