Generics and Trait Objects with same Trait Bound

When Generics meet Trait Objects of the same Trait Bound

I'm new to rust, and I'm stuck on a problem related to the implementation of a graph data structure. I'm also new to non-GC programming, so please forgive my ignorance. I've worked through most of The Book at this point, and I wanted to set out to make a real application.

Goal

I have a struct containing a generic list with a trait bound, which can only store objects of the same type because generics can only resolve to a single concrete type,

struct ComponentList<T>( HashMap<T: IsComponent> )

...that I want to gather into a manager list that can accept objects of different types which share the same trait using a trait object.

struct ComponentManager( HashMap<dyn IsComponentList> )

The final goal would be a ComponentManager that owns a bunch of ComponentLists, each with different concrete (IsComponent) types.

Issue

The issue occurs when I try to use this ComponentManager to manage the ComponentLists. I want to be able to add a component using ComponentManager::add_component() which will figure out which list to stick the component in based on its concrete type.

cm = ComponentManager::new();
some_component = SomeComponentType::new(); // Must implement IsComponent
// Stick this component in a ComponentList<SomeComponentType>
// Which is stored inside the ComponentManager
cm.add_component(some_component);

However I'm unable to use ComponentManager( HashMap<dyn IsComponentList> ) after I implement IsComponentList::add_component(). I receive these errors

"the trait ecs::IsComponentList cannot be made into an object"

"method add_component has generic type parameters"

Which seem to stem from the fact that add_component() uses the IsComponent trait bound.

pub trait IsComponentList {
    fn add_component(&mut self, component: impl IsComponent);
    ...
}

Attempted Solution

The errors lead me to this thread: Trait object with generic parameter - #2 by hashedone, suggesting I change the impl IsComponent to &dyn IsComponent. This results in even more issues, because I have to now swap out every instance I use the generic T:IsComponent with the trait object. I don't want this though - I want every component in a ComponentList to be of the same concrete type!

It feels like there must be a better way to accomplish the goal I've outlined in the first section. Ultimately, what I'm trying to achieve is a way to manage many objects of differing type in a memory efficient way. The problem seems to stem from the fact that my lists are defined with generics, and the list manager needs to use a trait object so it can store these many different types in the same list - but trait objects and generics are not compatible. I've tried restructuring my code a few times, and I've even restarted from scratch trying to approach from another angle.

Am I missing a more idiomatic solution? I know there are crates out there that accomplish this task, but I'm trying to learn why my approach isn't working, and how I need to update my mental model to accomplish similar tasks in the future - I want to understand!

Thank you!

What you are trying to do is not easy. I doubt real ecs do it like this, but I'm pretty sure the alternatives require unsafe code: link.

1 Like

Thanks for the reply @alice, the playground code is extremely helpful! I really like this approach to the problem.

What you are trying to do is not easy.

Out of curiosity, is this not a common pattern to need? I would think that creating and storing disparate objects in a containing structure you can query is something that would be necessary for any sort of relational object graph.

Sure, we store objects in collections, but this kind of type erasure is more or less not need anywhere but entity component systems.

Specifically the thing that is special here is that you don't want to entirely forget about the types: You still want things of the same type in the same list. If you just completely forget about the types, a box for each item solves that, but you want to go back and forth between remembering the type and not.

1 Like

I'm now wondering if there is a valid case for using my initial method. The idea was to reduce the set of objects needed to query to improve lookup performance for very large sets. I will generally know the type of item I'm looking for before I query, and there are so many types that knowing which set I'm looking in from the start should drastically reduce the search population.

In other words, you are optimizing, perhaps prematurely. Why not first get it working, then make these transformations and benchmark whether they are worth the additional complexity?

2 Likes

That's totally fair. :grinning:

I expected it would be necessary down the road for performance reasons, and wanted to avoid a painful refactor of core logic. Maybe I just need to Stop Worrying and Learn to Love the Type System.

In an attempt to revisit this problem and implement your feedback, I went to add a get_component function, similar to the add_component function you supplied. I attempted to use downcast() instead of downcast_mut() because I do not need to mutate the value. However, I kept getting the following error:

move occurs because *list has type std::boxed::Box<dyn std::any::Any>, which does not implement the Copy trait

playground link

Until I went and changed back to downcast_mut():
playground link

Why is this? I can't seem to make sense of why I would need to mutate the list in order to downcast?

The downcast function takes ownership. You can use downcast_ref instead.

1 Like

Thanks for clarifying - what is the purpose of downcast then? Am I just misusing it?

The purpose of downcast is to convert Box<dyn Any> into Box<T>, whereas downcast_ref turns &dyn Any into &T.

Thank you!

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