Question

So, in general casting and dynamic_cast in particular are to be avoided. But I don't see a proper alternative for this:

List<DerivedA*> ListA;
List<DerivedB*> ListB;

Bool Add(Base* obj)
{
  if(DerivedA* AsA = dynamic_cast<DerivedA*>(obj)
  {
    ListA.Add(AsA);
  }
  else if(DerivedB* AsB = dynamic_cast<DerivedB*>(obj)
  {
    ListA.Add(AsB);
  }
}

Most answers I've found, while looking for a solution suggest that this is basically a symptom of bad architecture, so here some context for my specific case.

I'm working on a small shooter where the player can carry different weapons with their own ammo type and different types of grenades. The amount of grenades & weapons to carry at the same time is limited.

As a visual representation I have a pickup actor that holds a reference to the actual item. That's where the Base* to some derived class is coming from. Now when collecting an item I basically need to add weapons and grenades to different inventories and for ammo I need to check whether the player has the respective weapon and increase the ammo count accordingly.

Alternatives usually mentioned are

  1. Using virtual methods: Which would mean Item.addTo(inventory* inv) But that requires items to know about the inventory implementation and if i would want to switch to some kind of RPG style inventory that stores all items in the same array I'd need to change every derived class.
  2. Visitor pattern; Used that for something else once. I didn't find a good solution to have some default implementation in case I don't need special behavior for a derived class and that aside it's very likely that new derived classes will be added, requiring to change all inventory classes each time.

So; what's wrong, what am I missing? Or is this actually a case where the above code is necessary?

Was it helpful?

Solution

You haven't missed anything. Some things are just difficult to do.

Dynamic casts are not inherently evil, they are just fragile: if you add a DerivedC type, your casts will continue to work but just skip objects of this type with no compile-time error. This is very analogous to the issue that an Object::addTo(Inventory&) method would need to be kept in sync with the structure of the inventory.

The core question is along which dimensions you want to keep changes easy, and along which dimensions you can sacrifice ease of change.

  • If you want to change the inventory system easily but not add new item types, dynamic casts or the visitor pattern appear sensible.
  • If you want to keep the inventory system fixed but easily add new item types, then Object::addTo(Inventory&) seems better.

The visitor pattern can sometimes help by separating objects in a hierarchy from operations on that hierarchy, in a manner that meshes well with static typing. The downside is that the hierarchy of objects becomes fixed and cannot be extended later without breaking existing visitors.

Trying to extend both the hierarchy and the available operations is called the expression problem and is very tricky. I think there has been some success with using C++ templates, but integrating such solutions with your game's data model is likely overkill.

For your scenario, all of these issues are tradeoffs but not dealbreakers: you're not making any change impossible, some changes just become more difficult. You can always refactor later. Personally:

  • I would use the visitor pattern based approach because I value static type checking very strongly.
  • Your original downcasting-based solution is equally good if you have a QA strategy that would likely find any missing cases. I would add an else-branch that logs any missing cases and (if in testing/debug mode) coredumps the program.
  • I would avoid the addTo() approach because this spreads knowledge about inventory management around the entire application (low cohesion, high coupling between different parts).

You should also consider whether it makes sense to model your DerivedA and DerivedB types as separate C++ classes. Especially for games, it can make more sense to sidestep the C++ type system and implement your own dynamically checked system. While that's more error-prone, it also provides for far more flexibility that (a) makes it easier to script interactions, and (b) enables more complex mechanics, e.g. a type of ammo that can also be used as a grenade. Please read the chapter Type Object Pattern in the book Game Programming Patterns.

OTHER TIPS

I'ma jump in with the rude/controversial answer here and say the whole problem here is object-oriented oriented programming (not a diss on OOP in general, but here). You are trying to make a game with complex object interactions to my understanding. So you conceive of data you need and hide it all behind objects with these nice abstract public interfaces with the priority of maintaining invariants over the internal states of these objects.

And then you try to conceive of these centralized abstract interfaces, except they don't do enough. They don't handle the case where some function that involves some complex interactions between objects like picking up ammo and checking whether the player has the respective weapon type or something like that... or checking whether it's raining to allow frogs to regenerate health... or your designer comes up with some new idea unfortunately 6 months into development for a new weapon that does something that breaks your whole mental model of how weapons should even work.

So you could try to keep expanding those public interfaces and make all relevant subtypes implement whatever functions you add to do the necessary computations and mutations. Except in the process, you very quickly create monolithic interfaces whose functions are not applicable to everything that implements/inherits them, or face the temptation to downcast left and right with dynamic_casts checking for things far more specific than the generalized interface you've built, losing much of the benefits of creating these abstractions in the first place and the polymorphism they allow... or make your abstractions so leaky in terms of their details that they can no longer maintain invariants at all. And when you've reached this point, in my mind it's not some series of design patterns that come to the rescue. It's realizing that the abstractions you built are frequently getting in the way of what you want to do rather than making it easier to do what you want to do... and humbly getting rid of them in favor of, say, a more procedural or functional approach in these cases where they fit better.

When you frequently want to x-ray your abstractions and reach around them, downcast away their generality to get to the concrete, I would start off really questioning if those abstractions should even exist. It's the most productive question I think one could ask. Are they really making your life easier, or are they just constantly getting in the way?

Licensed under: CC-BY-SA with attribution
scroll top