Finite State Machines#

The header <common/fsm.h> provides a template based framework to implement finite state machines in C++.

template<typename ...States>
class StateMachine#

Represents a Finite State Machine (FSM).

The FSM has a set of possible states and a set of rules that govern transitions between the current state and some other state upon receiving an event.

A state can be an arbitrary object, which type is used to distinguish it from other states. Therefore, there is no need to maintain a list of known states (in an enumeration for example). We also don’t want to enforce any relationship between those types to keep them completely independent. The state machine is a variadic template that gets to know its states through the parameter pack and stores them in a tuple.

The state machine keeps track of the current state as a std::variant (which can accommodate the diversity of state types) and assumes that the first state provided to it is the initial state. It is possible however to explicitly transition the state machine into a specific state using its TransitionTo() method.

Transitions from the current state to other states are triggered by events. This requires that all the state types have a Handle method that accepts an event of the type of event being handled. To avoid creating unnecessary dependencies between the state types and the state machine type, a state’s Handle method will return an object of a specific type (an action) that will describe what action should the machine take.

  [[nodiscard]] [[maybe_unused]] auto Handle(const UnlockEvent &event) const
      -> Maybe<TransitionTo<ClosedState>> {
    if (event.key == key_) {
      return TransitionTo<ClosedState>{};
    }
    return DoNothing{};
  }
In principle, a state machine should always transition to some state after receiving an event, even if it’s the same state as the current one. When we want to skip performing a transition action, we can simply use the helper action type ‘DoNothing’.

Events are produced outside the state machine and fed into it by an external event loop which calls the state machine’s Handle() method. The latter dispatches the event to the current state, which returns a specific action that gets executed to eventually transition to a new state. Event handlers, the action execution, and anything inside could throw exceptions to report errors that are all caught and translated into an execution status returned to the event production loop.

The execution status can take one of the following types documented in the class: Continue, Terminate, TerminateWithError and ReissueEvent. Refer to the documentation of these types and the StateMachine::Status.

Full Example

/*
 * This example simulates a door with an electronic lock. When the door
 * is locked, the user chooses a lock code that needs to be re-entered
 * to unlock it.
 */

struct OpenEvent {};
struct CloseEvent {};

struct LockEvent {
  uint32_t newKey; // the lock code chosen by the user
};

struct UnlockEvent {
  uint32_t key; // the lock key entered when unlocking
};

struct ClosedState;
struct OpenState;
struct LockedState;

struct ClosedState
    : Will<ByDefault<DoNothing>, On<LockEvent, TransitionTo<LockedState>>,
          On<OpenEvent, TransitionTo<OpenState>>> {};

struct OpenState
    : Will<ByDefault<DoNothing>, On<CloseEvent, TransitionTo<ClosedState>>> {};

struct LockedState : ByDefault<DoNothing> {
  using ByDefault::Handle;

  explicit LockedState(uint32_t key) : key_(key) {
  }

  [[maybe_unused]] auto OnEnter(const LockEvent &event) -> Status {
    key_ = event.newKey;
    return Continue{};
  }

  [[nodiscard]] [[maybe_unused]] auto Handle(const UnlockEvent &event) const
      -> Maybe<TransitionTo<ClosedState>> {
    if (event.key == key_) {
      return TransitionTo<ClosedState>{};
    }
    return DoNothing{};
  }

private:
  uint32_t key_;
};

using Door = StateMachine<ClosedState, OpenState, LockedState>;

// NOLINTNEXTLINE
TEST(StateMachine, ExampleTest) {
  Door door{ClosedState{}, OpenState{}, LockedState{0}};

  constexpr int lock_code = 1234;
  constexpr int bad_code = 2;

  door.Handle(LockEvent{lock_code});
  door.Handle(UnlockEvent{bad_code});
  door.Handle(UnlockEvent{lock_code});
}

Public Functions

inline explicit StateMachine(States... states)#

Construct a new State Machine object with the given states, starting at the first state in the list.

See also

TransitionTo() if you need to start at a specific state other than the first one in the list.

template<typename Event>
inline auto Handle(const Event &event) -> Status#

Dispatch the event to the current state and execute the returned action.

The state machine is animated by an external event loop that produces events, passed to this method. Each event is dispatched to the current state for handling, which returns an action object that get executed. The result of executing the action is translated into an execution status for the state machine that can be to continue processing more events, terminate with no error or terminate with error.

A special case is when an event results in a state transition but the new state prefers to handle the event in one of its handlers rather than in the OnEnter method. That particular situation corresponds to the execution status ReissueEvent which requires the event production loop to reissue that same event again.

template<typename State>
inline auto TransitionTo() -> State&#

Explicitly transition the State Machine to a specific state without executing a transition action.

This is provided for the sole purpose of setting the State Machine to a specific starting state from the external event production loop. Transitions from event handlers should exclusively use the TransitionTo action.

Template Parameters:

State – the state to which the State Machine should transition.

Returns:

State& the new state.

template<typename State>
inline auto IsIn() const -> bool#

Check if the State Machine is in a particular state.

Template Parameters:

State – the state to be checked for.

Returns:

true if the State Machine’s current state is the state being checked for; false otherwise.