c++inheritancesfmldynamic-binding

c++ particle system inheritance


i'm creating particle system and i want to have possibility to choose what kind of object will be showing on the screen (like simply pixels, or circle shapes). I have one class in which all parameters are stored (ParticleSettings), but without those entities that stores points, or circle shapes, etc. I thought that i may create pure virtual class (ParticlesInterface) as a base class, and its derived classes like ParticlesVertex, or ParticlesCircles for storing those drawable objects. It is something like that:

class ParticlesInterface
{
protected:
    std::vector<ParticleSettings>   m_particleAttributes;   

public:
    ParticlesInterface(long int amount = 100, sf::Vector2f position = { 0.0,0.0 });
    const std::vector<ParticleSettings>& getParticleAttributes() { return m_particleAttributes; }
...
}

and :

class ParticlesVertex : public ParticlesInterface
{
private:                            
    std::vector<sf::Vertex>         m_particleVertex;
public:
    ParticlesVertex(long int amount = 100, sf::Vector2f position = { 0.0,0.0 });
    std::vector<sf::Vertex>& getParticleVertex() { return m_particleVertex; }
...
}

So... I know that i do not have access to getParticleVertex() method by using polimorphism. And I really want to have that access. I want to ask if there is any better solution for that. I have really bad times with decide how to connect all that together. I mean i was thinking also about using template classes but i need it to be dynamic binding not static. I thought that this idea of polimorphism will be okay, but i'm really need to have access to that method in that option. Can you please help me how it should be done? I want to know what is the best approach here, and also if there is any good answer to that problem i have if i decide to make that this way that i show you above.


Solution

  • From the sounds of it, the ParticlesInterface abstract class doesn't just have a virtual getParticleVertex because that doesn't make sense in general, only for the specific type ParticlesVertex, or maybe a group of related types.

    The recommended approach here is: Any time you need code that does different things depending on the actual concrete type, make those "different things" a virtual function in the interface.

    So starting from:

    void GraphicsDriver::drawUpdate(ParticlesInterface &particles) {
        if (auto* vparticles = dynamic_cast<ParticlesVertex*>(&particles)) {
            for (sf::Vertex v : vparticles->getParticleVertex()) {
                draw_one_vertex(v, getCanvas());
            }
        } else if (auto* cparticles = dynamic_cast<ParticlesCircle*>(&particles)) {
            for (CircleWidget& c : cparticles->getParticleCircles()) {
                draw_one_circle(c, getCanvas());
            }
        }
        // else ... ?
    }
    

    (CircleWidget is made up. I'm not familiar with sf, but that's not the point here.)

    Since getParticleVertex doesn't make sense for every kind of ParticleInterface, any code that would use it from the interface will necessarily have some sort of if-like check, and a dynamic_cast to get the actual data. The drawUpdate above also isn't extensible if more types are ever needed. Even if there's a generic else which "should" handle everything else, the fact one type needed something custom hints that some other future type or a change to an existing type might want its own custom behavior at that point too. Instead, change from a thing code does with the interface to a thing the interface can be asked to do:

    class ParticlesInterface {
        // ...
    public:
        virtual void drawUpdate(CanvasWidget& canvas) = 0;
        // ...
    };
    
    class ParticlesVertex {
        // ...
        void drawUpdate(CanvasWidget& canvas) override;
        // ...
    };
    class ParticlesCircle {
        // ...
        void drawUpdate(CanvasWidget& canvas) override;
        // ...
    };
    

    Now the particles classes are more "alive" - they actively do things, rather than just being acted on.

    For another example, say you find ParticlesCircle, but not ParticlesVertex, needs to make some member data updates whenever the coordinates are changed. You could add a virtual void coordChangeCB() {} to ParticlesInterface and call it after each motion model tick or whenever. With the {} empty definition in the interface class, any class like ParticlesVertex that doesn't care about that callback doesn't need to override it.

    Do try to keep the interface's virtual functions simple in intent, following the Single Responsibility Principle. If you can't write in a sentence or two what the purpose or expected behavior of the function is in general, it might be too complicated, and maybe it could more easily be thought of in smaller steps. Or if you find the virtual overrides in multiple classes have similar patterns, maybe some smaller pieces within those implementations could be meaningful virtual functions; and the larger function might or might not stay virtual, depending on whether what remains can be considered really universal for the interface.

    (Programming best practices are advice, backed by good reasons, but not absolute laws: I'm not going to say "NEVER use dynamic_cast". Sometimes for various reasons it can make sense to break the rules.)