Finite State Machine helper class
Implementation of a finite state machine engine
Allows setting up a system of states and transitions between them. States are definedd as empty structs (footprint: 1 byte). Tranitions call functions using overload resolution. This works by subclassing this class, providing the deriving type as the first template argument (CRTP) and providing methods like
event_return on_event(const S&, const E&);
The arguments are the state S
and the triggered event E
. Their values
can be discarded (you can attach values to events of course, if you like)
The return type of these functions is effectively std::optional<State>
, so
you can either return std::nullopt
to remain in the same state, or an
instance of another state. That state will then become active.
You can also define a method template, which will serve as a catch-all handler (due to the fact that it will match any state/event combination):
template <typename State, typename Event>
event_return on_event(const State&, const Event&) const {
return Terminated{};
}
If for a given state and event no suitable overload of on_event
(and you
also haven't defined a catch-all as described above), a transition to
Terminated
will be triggered. This is essentially equivalent to the method
template above.
If this triggers, it will switch to the Terminated
state (which is always
included in the FSM).
Additionally, the FSM will attempt to call functions like
void on_enter(const State&);
void on_exit(const State&);
when entering/exiting a state if they are implemented. This can be used to
perform actions regardless of the source or destination state in a
transition to a given state. This is also fired in case a transition to
Terminated
occurs.
If the derived class implements
void on_process(const Event&);
void on_process(const State&, const Event&);
void on_process(const State1& const Event&, const State2&);
they are called during event processing, and allow for things like event and transition logging.
The public interface for the user of the FSM are the
template <typename... Args>
void setState(StateVariant state, Args&&... args);
template <typename Event, typename... Args>
void dispatch(Event&& event, Args&&... args) {
setState
triggers a transition to a given state, dispatch
triggers
processing on an event from the given state. Both will call the appropriate
on_exit
and on_enter
overloads. Both also accept an arbitrary number of
additional arguments that are passed to the on_event
, on_exit
and
on_enter
overloads.
It might become clearest in an example (taken from the unit test):
namespace states {
struct Disconnected {};
struct Connecting {};
struct Pinging {};
struct Connected {};
} // namespace states
namespace events {
struct Connect {};
struct Established {};
struct Timeout {};
struct Ping {};
struct Pong {};
struct Disconnect {};
} // namespace events
struct fsm : FiniteStateMachine<fsm, states::Disconnected, states::Connecting,
states::Pinging, states::Connected> {
fsm() : fsm_base(states::Disconnected{}){};
event_return on_event(const states::Disconnected&, const events::Connect&) {
return states::Connecting{};
}
event_return on_event(const states::Connecting&, const events::Established&) {
return states::Connected{};
}
event_return on_event(const states::Connected&, const events::Ping&) {
std::cout << "ping!" << std::endl;
setState(states::Pinging{});
return process_event(events::Pong{});
}
event_return on_event(const states::Pinging&, const events::Pong&) {
std::cout << "pong!" << std::endl;
return states::Connected{};
}
event_return on_event(const states::Connected&, const events::Timeout&) {
return states::Connecting{};
}
event_return on_event(const states::Connected&, const events::Disconnect&) {
return states::Disconnected{};
}
};
BOOST_AUTO_TEST_CASE(Transitions) {
fsm sm{};
BOOST_CHECK(sm.is(states::Disconnected{}));
sm.dispatch(events::Connect{});
BOOST_CHECK(sm.is(states::Connecting{}));
sm.dispatch(events::Established{});
BOOST_CHECK(sm.is(states::Connected{}));
sm.dispatch(events::Ping{}); // prints ping!, then pong!
sm.dispatch(events::Ping{}); // prints ping!, then pong!
sm.dispatch(events::Ping{}); // prints ping!, then pong!
sm.dispatch(events::Ping{}); // prints ping!, then pong!
BOOST_CHECK(sm.is(states::Connected{}));
sm.dispatch(events::Timeout{});
BOOST_CHECK(sm.is(states::Connecting{}));
sm.dispatch(events::Established{});
BOOST_CHECK(sm.is(states::Connected{}));
sm.dispatch(events::Disconnect{});
BOOST_CHECK(sm.is(states::Disconnected{}));
}
I want to use this for a potential rewrite of the Navigator and the Kalman Actor, so want to get this in independently of any other changes. Let me know what you think!