diff --git a/include/mtl/fsm.hpp b/include/mtl/fsm.hpp index c24054e..df30522 100644 --- a/include/mtl/fsm.hpp +++ b/include/mtl/fsm.hpp @@ -1,42 +1,121 @@ #pragma once -#include +#include +#include namespace mtl { + +/** + * \brief FSM Event + * + * Base class each FSM event should be derived from. Derived classes are passed + * to fsm.dispatch(). Empty unless members are added in the derived class. + */ class fsm_event { private: public: }; -template -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 +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 -class fsm { + fsm_state() {} - FSMType* state = nullptr; - -protected: virtual void react(const mtl::fsm_event& event) = 0; virtual void entry() {} 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 void change_state() { - state = &fsm_state_instance; + m_fsm->template change_state(); + } +}; + +// 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 +class fsm { +private: + friend fsm_state; + + StateType* m_cur_state = nullptr; + /* + * TODO: Possibly use different tuple implementation? + */ + std::tuple 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 + void change_state() { + m_cur_state->exit(); + m_cur_state = &std::get(m_states); + m_cur_state->entry(); } public: fsm() { - state = &fsm_state_instance; - } + // 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(this)), ...); + }, m_states); + // Set the initial state to the first type in FSMStates + m_cur_state = &std::get<0>(m_states); + } + template - void dispatch(const T& event) { - state->react(event); + void dispatch(const T& event = T()) { // Use default arg to allow dispatch() + m_cur_state->react(event); // with default constructor } }; - } // namespace mtl