I decided to try a hand at writing a simple state machine. The StateMachine class accepts any number of classes as template arguments, along with a base class. The classes act as states (a class so it can have permanent data), which have enter, exit and tick functions. The tick function can return nullptr to stay on the current state or another class object to transition to another state. exit and enter are called appropriately.
The StateMachine class contains a StateContainer class that houses the actual state objects as an array of std::unique_ptr<Base>. When StateMachine is constructed, the constructor on StateContainer instantiates an array of pointers to the classes provided. These pointers are only deleted when the StateContainer class is destroyed. StateContainer provides a getState<T> function, which is intended to be used inside each state's tick. It looks up the given class T inside the state array and returns it, allowing one to simply return getState<NewState>(); inside tick.
One thing I don't much like about this is how the state classes are given the StateContainer. You have to forward declare all states, define the StateContainer type, and then use that in the state classes. It feels like a code smell but I don't know any other way around it. I tried a variation using std::vector<std::any> but that meant I had to use std::any_cast(T*) and I don't fancy that in a real time system.
An earlier version of this (written in C#) had a hard-coded StateContainer where every state was just a public property. I didn't quite like that limitation, hence the iteration of StateContainer here, but it gets around the above problems.
template<class StateBase, class... StateTypes> struct StateContainer { private: constexpr static int state_count = sizeof...(StateTypes); std::array<std::unique_ptr<StateBase>, state_count> _states; template<int n, class Match, class HeadState, class... TailStates> StateBase* getStateHelper() { if constexpr (std::same_as<Match, HeadState>) { return _states[n].get(); } else if constexpr (sizeof...(TailStates) > 0) { auto x = getStateHelper<n + 1, Match, TailStates...>(); return x; } else { return nullptr; } } public: StateContainer() { int i = 0; ((_states[i++] = std::make_unique<StateTypes>(*this)), ...); } template<class T> T* getState() { auto x = this->getStateHelper<0, T, StateTypes...>(); return static_cast<T*>(x); } }; template<class T, class StateContainer> concept is_state_constructible = std::constructible_from<T, StateContainer&>; template<class StateBase, class... StateTypes> requires (std::derived_from<StateTypes, StateBase> && ...) && (is_state_constructible<StateTypes, StateContainer<StateBase, StateTypes...>> && ...) class StateMachine { public: using StateContainerType = StateContainer<StateBase, StateTypes...>; private: StateContainerType _states; StateBase* _current; public: StateMachine() : _current(nullptr) { } void setState(StateBase* state) { if (_current != nullptr) { _current->exit(); } _current = state; if (state != nullptr) { state->enter(); } } void tick() { if (_current != nullptr) { if (auto newState = _current->tick(); newState != nullptr) { setState(newState); } } } StateContainerType& states() { return _states; } }; class StateBase; struct s_one; struct s_two; struct s_three; using StateContainerT = StateContainer<StateBase, s_one, s_two, s_three>; class StateBase { private: StateContainerT* _states; protected: template<class T> T* getState() { return _states->getState<T>(); } public: StateBase(StateContainerT& states) : _states(&states) { } virtual ~StateBase() { } virtual void enter() = 0; virtual StateBase* tick() = 0; virtual void exit() = 0; }; struct s_one final : public StateBase { public: s_one(StateContainerT& states) : StateBase(states) { } int x = 1; void enter() override { } StateBase* tick() override { return nullptr; }; void exit() override { } }; struct s_two final : public StateBase { public: s_two(StateContainerT& states) : StateBase(states) { } int y = 2; void enter() override { } StateBase* tick() override { return getState<s_one>(); }; void exit() override { } }; struct s_three final : public StateBase { public: s_three(StateContainerT& states) : StateBase(states) { } int z = 3; void enter() override { } StateBase* tick() override { return getState<s_two>(); }; void exit() override { } }; int main() { StateMachine<StateBase, s_one, s_two, s_three> sm; auto s = sm.states().getState<s_three>(); sm.setState(s); sm.tick(); sm.tick(); sm.tick(); return 0; }
any_castis an efficient cast unlikedynamic_cast. It doesn't rely on polymorphism but it is also weaker, as you need to know exact type. \$\endgroup\$type_infomatches. It's not specified how it actually does that. I believe it is possible to implement it efficiently so the string comparisons are unnecessary most of not all of the time, but no idea if they do anything of the sort. \$\endgroup\$any. \$\endgroup\$std::tuple. \$\endgroup\$