Search code examples
c++compositionstd-variant

Techniques for cutting down on verbosity when do polymorphism via std::variant rather than inheritance


Say you have entities in a 2D game framework or something similar -- e.g. a GUI framework -- where there are various types of entities that share common properties like position and rotation but where some of these properties must be handled on a per-entity type basis e.g. rotating a simple sprite is performed differently than rotating a rigged 2D animation skeleton.

Obviously this could be handled by a traditional OOP inheritance hierarchy ... However, I'm interested instead in representing such entities using "composition over inheritance" by having a concrete class that nothing inherits from called actor that has vanilla member variables for state that is handled the same way across entity types but also has a has-a relationship with a variant containing the state that must be handled in a per-entity type way.

This design can be made to work as below:

#include <iostream>
#include <variant>

struct actor_state_1 {
    float rotation_;
    //point position;
    // etc...

    void set_rotation(float theta) {
        rotation_ = theta;
        // say there are different things that need to happen here 
        // for different actor types...
        // e.g. if this is an animation skeleton you need to find the
        // the root bone and rotate that, etc. 
        std::cout << "actor_state_1 set_rotation\n";
    }

    void set_position(const std::tuple<float, float>& pos) {
        // etc ...
    }

    float get_rotation() const {
        return rotation_;
    }

    // get_position, etc...
};

struct actor_state_2 {
    float rotation_;

    void set_rotation(float theta) {
        rotation_ = theta;
        std::cout << "actor_state_2 set_rotation\n";
    }

    void set_position(const std::tuple<float, float>& pos) {
        // etc ...
    }

    float get_rotation() const {
        return rotation_;
    }

    // get_position, etc...
};

using state_variant = std::variant<actor_state_1, actor_state_2>;

class actor {
private:
    state_variant state_;
    // common properties...
    float alpha_transparency; // etc.
public:

    actor(const actor_state_1& state) :
        state_(state)
    {}

    actor(const actor_state_2& state) :
        state_(state)
    {}

    void rotate_by(float theta) {
        auto current_rotation = get_rotation();
        std::visit(
            [current_rotation, theta](auto& a) { a.set_rotation(current_rotation + theta); },
            state_
        );
    }

    float get_rotation() const {
        return std::visit(
            [](const auto& a) {return a.get_rotation(); },
            state_
        );
    }

    void move_by(const std::tuple<float, float>& translation_vec); 
    std::tuple<float, float> get_postion() const; // etc.

};

int main() {
    auto a = actor(actor_state_2{ 90.0f });
    a.rotate_by(45.0f);
    std::cout << a.get_rotation() << "\n";
}

However I feel as though the level of verbosity and the amount of repeated boilerplate code per property makes such a design unwieldy. I however can't figure out a way to cut down on the boilerplate by using templates. It seems like there should be a way to at least make a template for "pass-through getters" like actor::get_rotation() in the above but I don't know a way of parametrizing on a member function which is what it seems like you would need to do.

Does anyone have ideas for a design like this that is less verbose or uses less boilerplate?


Solution

  • Just use a visitor:

    #include <variant>
    
    struct state_a{};
    struct state_b{};
    
    struct actor
    {
       std::variant<state_a, state_b> state;
    };
    
    // basically no need to declare them as members.
    void rotate(state_a, double deg)
    {
      // do a
    }
    
    void rotate(state_b, double deg)
    {
      // do b
    }
    
    void rotate(actor& a, double deg)
    {
      std::visit([deg](auto &state){ rotate(state, deg); }, a.state);
    }
    

    Here is a more scalable version with templates: https://godbolt.org/z/58Kjx9h5W

    // actor.hpp
    #include <variant>
    
    template <typename ... StatesT>
    struct actor
    {
       std::variant<StatesT...> state;
    };
    
    template <typename StateT>
    void rotate(StateT& s, double deg)
    {
        return rotate(s, deg);
    }
    
    template <typename ... StatesT>
    void rotate(actor<StatesT...> &a, double deg)
    {
        std::visit([deg](auto &state){ rotate(state, deg); }, a.state);
    }
    
    // other headers
    #include <iostream>
    
    struct state_a{};
    struct state_b{};
    
    // basically no need to declare them as members.
    void rotate(state_a, double deg)
    {
        // do a
        std::cout << "rotating a\n";
    }
    
    void rotate(state_b, double deg)
    {
        // do b
        std::cout << "rotating a\n";
    }
    
    int main()
    {
        auto actr = actor<state_a, state_b>{state_a()};
        rotate(actr, 30);
    }
    

    Be advised, however, that "recursive" template <typename StateT> void rotate(StateT& s, double deg) will not work if you use a namespace for your states because of ADL.
    You either declare rotate AND state_a in the same namespace.