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_cast
is 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_info
matches. 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\$