Table of contents:
This is my second take on C++ state machine implementation. For those who are interested, my first attempt can be found here. The main goals of the implementation are:
- No dynamic allocations
- Sub-state support
- Type safety
- Limited stdlib dependencies (in theory easier to port on embedded platforms)
- It's not pure FSM, may execute any code/logic in event handlers
It is still not a production grade software, use with caution, report any issues in the "Issues" page.
Currently only C++17 capable compiler is required with some part of standard library: type_traits, variant and tuple.
You can start with a more complex example or/and read through below introduction section.
State is a class derived from fsmpp2::state<> template.
struct A_State : fsmpp2::state<> {};
A state class instance is created when State Machine enters that state and is destructed when it leaves. This concept greatly increase imporatance and usage of RAII idiom. Eg. you can hold a lock or open file while in state and rely on a fact that it will be destructed when SM leaves that state. State handle events by using overloaded handle() method accepting particular (event) type.
struct A_State : fsmpp2::state<> {
auto handle(AnEvent const&) {
return handled();
}
};
States are not heap-allocated, state machine holds all states in std::variant.
While states are short living objects and the user have little to no control over how they are managed it is possible to pass a custom object reference (Context) to a state. Context is an arbitrary object that serves as long living shared data space. There are several ways to access a common Context from a state
To pass a context down to state machine state you need to declare it in state_machine template. There are two ways of doing that, explicitly parametrize the template:
using States = fsmpp2::states<StateA, StateB>;
using Events = fsmpp2::events<Ev1>;
Context ctx;
fsmpp2::state_machine<States, Events, Context &> sm{ctx};
// or, to instantiate Context within state_machine template itself, omit the reference argument
fsmpp2::state_machine<States, Events, Context> sm;
or rely on CTAD:
using States = fsmpp2::states<StateA, StateB>;
using Events = fsmpp2::events<Ev1>;
Context ctx;
fsmpp2::state_machine sm{States{}, Events{}, ctx};
In case you need different type of contexts in different states you can declare a set of contexts:
struct CtxA;
struct CtxB;
struct State;
CtxA ctx_a;
CtxB ctx_b;
fsmpp2::state_machine sm {
fsmpp2::states<State>{},
fsmpp2::events<Ev1>{},
fsmpp2::contexts{ctx_a, ctx_b}
};
Context can be accessed from within state constructor or/and even handler.
struct CustomContext {};
struct A_State : fsmpp2::state<> {
A_State(CustomContext &) {}
};
struct CustomContext { int var = 0; };
struct A_State : fsmpp2::state<>
{
auto handle(Event, CustomContext& ctx) {
ctx.var = 42;
return handled();
}
};
If there are multiple contexts in the state machine access_context
may be use to access multiple contexts in constructor or event handler, eg:
struct CtxA;
struct CtxB;
struct CtxC;
struct State : fsmpp2::state<>
{
State(fsmpp2::access_context<CtxA> c)
{
c.get_context().member;
}
auto handle(EventX, fsmpp2::access_context<CtxB, CtxC> c)
{
c.get_context<CtxB>().b_member;
c.get_context<CtxC>().c_member;
return handled();
}
};
"State machine" is simply a set of states, events and a context
struct StateA;
struct StateB;
struct StateC;
struct Ev1;
struct Ev2;
struct CommonContext;
using States = fsmpp2::states<StateA, StateB, StateC>;
using Events = fsmpp2::events<Ev1, Ev2>;
CommonContext ctx;
fsmpp2::state_machine<States, Events, CommonContext> sm{ctx};
by default, SM enters the first state on the list, in this case StateA
.
Event passing is done by calling dispatch() method on SM object:
fsmpp2::state_machine<...> sm;
sm.dispatch(AnEvent{});
In order to move from one state to another a state handle() method needs to indicate that by returing a special value, the simplest case is:
struct StateA : fsmpp2::state<> {
auto handle(AnEvent const&) const {
return transition<StateB>();
}
};
// ...
fsmpp2::state_machine<fsmpp2::states<StateA, StateB, StateC>, fsmpp2::events<AnEvent>, Context> sm;
sm.dispatch(AnEvent{}); // transits from StateA to StateB
If there are multiple return
paths, return value needs to be explicitly stated (as there's no way to auto-deduce it).
struct StateA : fsmpp2::state<> {
auto handle(AnEvent const& e) const -> fsmpp2::transitions<StateB, StateC> {
if (e.some_value == 42)
return transition<StateB>();
else if (e.some_value == 3)
return transition<StateC>();
else
return handled();
}
};
// ...
fsmpp2::state_machine<fsmpp2::states<StateA, StateB, StateC>, fsmpp2::events<AnEvent>, Context> sm;
sm.dispatch(AnEvent{3}); // transits from StateA to StateC
for the sake of clarity you may opt to declare your event handlers as
struct StateA : fsmpp2::state<> {
auto handle(EventA const&) -> fsmpp2::transitions<>;
auto handle(EventB const&) -> fsmpp2::transitions<StateB>;
auto handle(EventC const&) -> fsmpp2::transitions<StateC>;
auto handle(EventD const&) -> fsmpp2::transitions<StateB, StateC, StateD>;
};
so it's clerly visible in the state class interface which event can lead to what transition.
The library supports state hierarchy but this sections is "To be described". For more information see an example.
There's experimental support for PlantUML state diagrams (currently supported only when compiled with GCC). There are two type of diagrams that can be created with fsmpp2:
- state diagram
- (runtime) sequence diagram
Having a state set and event set the fsmpp2::plantuml::print_state_diagram<States, Events>(std::cout);
function is able to output PlantUML source code for diagram generation to std::ostream
, eg:
namespace events
{
struct Ev1 : fsmpp2::event {};
struct Ev2 : fsmpp2::event {};
struct Ev3 : fsmpp2::event {};
} // namespace events
namespace states
{
struct A;
struct B;
struct C;
struct D;
struct E;
struct F;
struct A : fsmpp2::state<>
{
auto handle(events::Ev1 const&) -> fsmpp2::transitions<B>;
};
struct B : fsmpp2::state<>
{
auto handle(events::Ev1 const&) -> fsmpp2::transitions<C>;
auto handle(events::Ev2 const&) -> fsmpp2::transitions<A>;
};
struct C : fsmpp2::state<>
{
auto handle(events::Ev1 const&) -> fsmpp2::transitions<A, D>;
auto handle(events::Ev2 const&) -> fsmpp2::transitions<F>;
};
struct D : fsmpp2::state<>
{
auto handle(events::Ev3 const&) -> fsmpp2::transitions<E>;
};
struct E : fsmpp2::state<>
{
auto handle(events::Ev3 const&) -> fsmpp2::transitions<F>;
};
struct F : fsmpp2::state<> {};
} // namespace states
int main()
{
using States = fsmpp2::states<states::A, states::B, states::C, states::D, states::E, states::F>;
using Events = fsmpp2::events<events::Ev1, events::Ev2, events::Ev3>;
fsmpp2::plantuml::print_state_diagram<States, Events>(std::cout);
}
when run and parsed by plantuml produce:
Another one, previously linked microwave example auto-generated example:
This does not require instantiation of the state machine object therefore can be easily factored out to a separate function or target (eg. CI updating state diagrams every build).
This is a combination of Tracer functionality (basically a trace logger following state machine execution in runtime) with a specialized type of formatting. It can provide kind of a log (state transitions, events) in a form of sequence diagram. It is not strictly a "specification" type of a diagram but can be useful for debugging or generating diagrams out of unit tests.
sm::ContextData ctx;
std::ofstream ofs{"sequence.txt"};
fsmpp2::state_machine sm{States{}, Events{}, ctx, fsmpp2::plantuml::seq_diagrm_trace{ofs}};
// run a scenarion
sm.tracer().begin();
sm.dispatch(...);
...
sm.tracer().end();
MIT License, for details see LICENSE file.