3
\$\begingroup\$

This might sound strange, but let me explain. Say I want to write a safari program.

In this safari there is an object pool of 10 animals (Animal*): 2 ostrich, 5 mosquitos, 2 spiders and a snake. Each of these inherits Animal. Both snake and spider are venomous so they have data reflecting it—let's say they additionally inherit IVenomous. The mosquito inherits IFlyer. The ostrich is left out in the cold with none of these additional features.

With venomous animals around, we'd want to prepare antivenom for the safari. So let's have prepareAntidote(). This function's job would be to check what kind of antivenom is needed and simply print it out. As part of implementing IVenomous, spider and snake each have the method const std::string& getVenomName(). The base animal class doesn't have this feature.

The issue then becomes: how do we access the getVenomName() method when all we know we have is a list of Animal, and we don't know which ones implement IVenomous and have a getVenomName() method available? What's considered good practice here?

I've considerd a brute force approach of making use of dynamic_cast<IVenomous*> while iterating over all animals, but this seems wasteful. And as bad is the fact that prepareAntidote() function needs to know what an animal is to handle the pool. So it would most probably be better for it to work on a container<IVenomous*>&, wouldn't it?

My idea is to have each interface's constructor register itself with some storage class which keeps a container of pointers for each interface, and de-registers itself on destruction, so we could access a list of IVenomous animals directly. So far an unordered_map looks the most reasonable for that job, with roughly constant time for access and removal, as well as not invalidating anything except on rehash. The pointer being its own key value.

It's actually the same problem with composition like components for example in games. (That's why I'm writing it here, because of that, chances may be higher to find answers here.) In this case it screams for "object pooling" of course, but let's cast aside memory locality in this case, that should be quite easy to add in later with overloading operator.

\$\endgroup\$
4
  • 2
    \$\begingroup\$ It sounds like you have an idea for how you want to handle this situation already. Have you observed a problem with handling it that way? What specific aspect can we help you improve? \$\endgroup\$
    – DMGregory
    Mar 24, 2021 at 12:44
  • \$\begingroup\$ Especially the "good practice" part. A lot of things that seem "natural" or intuitive at first glance aren't optimal in the long run. (for example blob-classes etc.) So I want to know if that approach/way of thinking may be a red flag for more experienced people and why. Or in that case what a better way to achieve it would be. \$\endgroup\$
    – Tminator
    Mar 24, 2021 at 14:15
  • 3
    \$\begingroup\$ That sounds like a Code Review question then, if you can exhibit a specific implementation. On GDSE, I generally find questions asking about future problems that could occur don't get very definitive answers. We've got a better track record for solving specific problems you can exhibit. So my usual advice is to try a prototype of your idea, and see if any problems arise for your game's particular use cases. If not, you're golden, even if another solution might be "better" for someone else. \$\endgroup\$
    – DMGregory
    Mar 24, 2021 at 15:08
  • \$\begingroup\$ The best practice is the one that works for your own specific situation. As highlighted by DMGregory, it seems you're doing some kind of "premature optimization". Do you have thousands of objects of a few hundreds? How long does it take to simulate the physics, render the scene? The time taken to updating your objects may be insignificant compared to other tasks, so whether you dynamic_cast or shove a type property to your Animal class may be completely irrelevant right now and not that useful when trying to address actual issues. \$\endgroup\$
    – Vaillancourt
    Mar 24, 2021 at 15:36

2 Answers 2

1
\$\begingroup\$

There's nothing wrong with keeping a container of derived class observing pointers for direct and immediate access while keeping an overall base class container for management:

#include <vector>

#include "Animal.hpp"
#include "Snake.hpp"
#include "Spider.hpp"
#include "Mosquito.hpp"
#include "IVenomous.hpp"
#include "IFlyer.hpp"

std::vector<std::unique_ptr<Animal>> m_Animals{};
std::vector<IVenomous*> m_VenomousAnimals{};
std::vector<IFlyer*> m_FlyingAnimals{};

void RegisterNewSnake() {
    auto newSnake = std::make_unique<Snake>();
    auto* ptr = newSnake.get();
    m_Animals.emplace_back(std::move(newSnake));
    m_VenomousAnimals.push_back(ptr);
}

void RegisterNewSpider() {
    auto newSpider = std::make_unique<Spider>();
    auto* ptr = newSpider.get();
    m_Animals.emplace_back(std::move(newSpider));
    m_VenomousAnimals.push_back(ptr);
}

void RegisterNewMosquito() {
    auto newMosquito = std::make_unique<Mosquito>();
    auto* ptr = newMosquito.get();
    m_Animals.emplace_back(std::move(newMosquito));
    m_FlyingAnimals.push_back(ptr);
}

void RegisterNewOstrich() {
    auto newOstrich = std::make_unique<Ostrich>();
    m_Animals.emplace_back(std::move(newOstrich));
}

std::vector<IVenomous*> GetAllVenomousAnimals() {
    return m_VenomousAnimals;
}

std::vector<IFlyer*> GetAllFlyingAnimals() {
    return m_FlyingAnimals;
}

void RemoveVenomousAnimal(IVenomous* ptr) {
    m_VenomousAnimals.erase(std::remove(std::begin(m_VenomousAnimals), std::end(m_VenomousAnimals), ptr), std::end(m_venomousAnimals));
    m_Animals.erase(std::remove_if(std::begin(m_Animals), std::end(m_Animals), [ptr](const auto& a){ a.get() == ptr; }), std::end(m_Animals));
}

//Similar for RemoveSnake

//Similar for RemoveSpider

void RemoveFlyer(IFlyer* ptr) {
    m_FlyingAnimals.erase(std::remove(std::begin(m_FlyingAnimals), std::end(m_FlyingAnimals), ptr), std::end(m_FlyingAnimals));
    m_Animals.erase(std::remove_if(std::begin(m_Animals), std::end(m_Animals), [ptr](const auto& a){ a.get() == ptr; }), std::end(m_Animals));
}

//Similar for RemoveMosquito

``` 
\$\endgroup\$
0
0
\$\begingroup\$

If it is important is an animal venomous and how much, document it! Add getVenomName(){ return "harmless"} to the base class.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .