Completely rewrite FSM implementation

Previously, each FSM class could only have one instance because of the
use of globals. This new implementation only uses memory allocated on
the stack, so multiple instances can be created at once. Dynamic
allocation is still unused. Additionally, this approach uses a more
logical separation between the FSM, states, and events.
This commit is contained in:
Myles Busig 2024-07-21 21:55:13 -06:00
parent c1cdde88c8
commit f4e4ef353a

View File

@ -1,42 +1,121 @@
#pragma once #pragma once
#include <cstdint> #include <tuple>
#include <mtl/log.hpp>
namespace mtl { namespace mtl {
/**
* \brief FSM Event
*
* Base class each FSM event should be derived from. Derived classes are passed
* to fsm.dispatch<FSMEvent>(). Empty unless members are added in the derived class.
*/
class fsm_event { class fsm_event {
private: private:
public: public:
}; };
template <typename T> /**
T fsm_state_instance; * \brief FSM State
*
* Base class each FSM state interface should be derived from. A FSM's state
* interface should provide an empty virtual overload of `react` for each event that is supported.
* Then, each FSM state derives from this state interface and overrides the
* event overloads as needed.
*
* FSMType = The FSM context type
*/
template <typename FSMType>
class fsm_state {
public:
/**
* Set in `fsm` constructor. We're not able to use a constructor with
* signature `fsm_state(FSMType*)` because std::tuple does not support
* passing to constructors (as far as I'm aware).
*/
FSMType* m_fsm = nullptr;
template <typename FSMType, typename InitState> fsm_state() {}
class fsm {
FSMType* state = nullptr;
protected:
virtual void react(const mtl::fsm_event& event) = 0; virtual void react(const mtl::fsm_event& event) = 0;
virtual void entry() {} virtual void entry() {}
virtual void exit() {} virtual void exit() {}
/*
* \brief Changes the current state
*
* `exit` is called on the current state, the state is switched to the
* desired state, and `entry` is called on the new state.
*
* Should not be used inside `entry` or `exit` because `entry` and `exit`
* are called on the entered/exited states.
*/
template <typename T> template <typename T>
void change_state() { void change_state() {
state = &fsm_state_instance<T>; m_fsm->template change_state<T>();
}
};
// We need the state base class type for overriding of overloaded `react`
/**
* \brief FSM Context
*
* FSMType = CRTP derived class
* StateType = state interface type
* FSMStates = list of 1 or more valid states
*
* All memory is allocated on the stack where the FSM is instantiated.
*
* The FSM state is initialized to the first state in FSMStates.
*/
template <typename FSMType, typename StateType, typename... FSMStates>
class fsm {
private:
friend fsm_state<FSMType>;
StateType* m_cur_state = nullptr;
/*
* TODO: Possibly use different tuple implementation?
*/
std::tuple<FSMStates...> m_states;
/*
* \brief Changes the current state
*
* NOTE: SHOULD NOT BE CALLED DIRECTLY. CALL FROM
* `fsm_state::change_state` INSTEAD.
*
* `exit` is called on the current state, the state is switched to the
* desired state, and `entry` is called on the new state.
*
* Should not be used inside `entry` or `exit` because `entry` and `exit`
* are called on the entered/exited states.
*/
template <typename T>
void change_state() {
m_cur_state->exit();
m_cur_state = &std::get<T>(m_states);
m_cur_state->entry();
} }
public: public:
fsm() { fsm() {
state = &fsm_state_instance<InitState>; // Set the FSM instance of each state using lambda+pack expansion
std::apply([this](auto&... states){ // std::apply calls the lambda
// Pack expansion sets m_fsm for each state
((states.m_fsm = static_cast<FSMType*>(this)), ...);
}, m_states);
// Set the initial state to the first type in FSMStates
m_cur_state = &std::get<0>(m_states);
} }
template <typename T> template <typename T>
void dispatch(const T& event) { void dispatch(const T& event = T()) { // Use default arg to allow dispatch<my_event>()
state->react(event); m_cur_state->react(event); // with default constructor
} }
}; };
} // namespace mtl } // namespace mtl