object oriented – how to avoid cascading cynamic casts?

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.