Motivation to create another SM library was to have well implemented anonymous transitions and more straightforward approach to composite state machines. dsml aims to be almost UML compliant.
- Header only.
- Lightning fast.
- Minimal resulting binary footprint and speed - don't pay for what you don't need.
- Safe - many compile-time checks.
- Clear semantics.
- State machine can be embedded into a custom class.
- Well tested.
- C++14 syntax
- few headers from STL
If your compiler supports templated user-defined literals with string argument
(e.g. gcc and clang) then you can represent states with a custom literal with a
suffix _s
and events with _e
.
#include <iostream>
#include <dsml.hpp>
static constexpr auto guard = [](){ return true; };
static constexpr auto action = [](){ std::cout << "hello\n"; };
struct MyMachine
{
auto operator()() const noexcept
{
using namespace dsml::literals;
return dsml::make_transition_table(
dsml::initial_state + "evt1"_e = "A"_s
, "A"_s + "evt2"_e = "B"_s
, "A"_s + "evt3"_e [ guard ] = "C"_s
, "B"_s + "evt4"_e / action = "C"_s
, "B"_s + "evt1"_e = "B"_s
, "C"_s = "D"_s
);
}
};
int main()
{
using namespace dsml::literals;
dsml::Sm<MyMachine> sm{};
sm.process_event("evt1"_e);
sm.process_event("evt2"_e);
sm.process_event("evt4"_e);
return sm.is("D"_s);
}
If you prefer identifiers (which makes the code less prone to typos) or you can't use those fancy user literals (e.g. MSVC) then use this notation:
#include <iostream>
#include <dsml.hpp>
static constexpr auto guard = [](){ return true; };
static constexpr auto action = [](){ std::cout << "hello\n"; };
#define STATE(x) static constexpr auto x = dsml::State<struct x##_>{}
STATE(A);
STATE(B);
STATE(C);
STATE(D);
#define EVENT(x) static constexpr auto x = dsml::Event<struct x##_>{}
EVENT(evt1);
EVENT(evt2);
EVENT(evt3);
EVENT(evt4);
struct MyMachine
{
auto operator()() const noexcept
{
return dsml::make_transition_table(
dsml::initial_state + evt1 = A
, A + evt2 = B
, A + evt3 [ guard ] = C
, B + evt4 / action = C
, B + evt1 = B
, C = D
);
}
};
int main()
{
dsml::Sm<MyMachine> sm{};
sm.process_event(evt1);
sm.process_event(evt2);
sm.process_event(evt4);
return sm.is(D);
}
// anonymous
"A"_s = "B"_s
// with event
"A"_s + "evt1"_e = "B"_s
const auto guard = [](){ return true; };
// anonymous transition
"A"_s [ guard ] = "B"_s
// with event
"A"_s + "evt1"_e [ guard ] = "B"_s
You can combine guards into a logical expression:
// add this into your scope to enable guard logical expressions
using namespace dsml::operators;
// with event
"A"_s + "evt1"_e [ guard1 && guard2 || !guard3 ] = "B"_s
const auto action = [](){ do_something(); };
// anonymous transition
"A"_s / action = "B"_s
// with event
"A"_s + "evt1"_e / action = "B"_s
Beware: If you call process_event()
inside the action then you will
get undefined behaviour because the action is called "in between" states.
You can chain multiple actions together. First you have to "include" operators:
// add this into your scope to enable actions chaining
using namespace dsml::operators;
Then you can chain actions separated with commas. Parentheses must be around the group.
"A"_s + "evt1"_e / (action1, action2, action3) = "B"_s
const auto action = [](){ do_something(); };
"A"_s + dsml::on_entry / action,
"A"_s + dsml::on_exit / action
Processing an event leading to the same state will invoke exit/entry actions too. E.g. :
"A"_s + dsml::on_entry / action_entry,
"A"_s + dsml::on_exit / action_exit,
"A"_s + "evt1"_e = "A"_s
Calling process_event("evt1"_e)
in the state A will call action_exit()
and action_entry()
.
const auto guard = [](){ return true; };
const auto action = [](){ do_something(); };
// anonymous transition
"A"_s [ guard ] / action = "B"_s
// with event
"A"_s + "evt1"_e [ guard ] / action = "B"_s
"A"_s + "evt1"_e = "B"_s
, "A"_s + "evt2"_e = "C"_s
, "A"_s + dsml::unexpected_event = "D"_s
Calling process_event
function with any event other than "evt1"_e
or
"evt2"_e
will transition to state D
. Unexpected event has lower priority.
If you have an event that leads to the same state from every other state then
use any_state
. You can combine it with unexpected_event
:
"A"_s + "evt1"_e = "B"_s
, dsml::any_state + "evt1"_e = "C"_s // lower priority
, dsml::any_state + dsml::unexpected_event = "D"_s // lowest priority
Useful to connect the SM with non-global logic. You can use lambdas (or free functions) that will accept dependencies as arguments or you can pass member function pointers.
Use dsml::callee()
to wrap member function pointers.
struct Logic
{
int x{};
bool dguard() const
{
return x < 99;
}
void daction(int& num)
{
x = 33;
num += 3;
}
};
struct MyMachine
{
auto operator()() const noexcept
{
using dsml::callee;
using namespace dsml::literals;
using namespace dsml::operators;
const auto guard = [](const Logic& logic){ return logic.x <= 5; };
const auto action = [](Logic& logic, int& num){ logic.x += 2; num += 10; };
return dsml::make_transition_table(
dsml::initial_state + "evt1"_e [ guard ] / action = "A"_s
, dsml::initial_state + "evt1"_e [ ! guard ] = "B"_s
, dsml::initial_state
+ "evt1"_e [ callee(&Logic::dguard) ] / callee(&Logic::daction)
= "B"_s
);
}
};
void func()
{
using namespace dsml::literals;
Logic logic{};
int num{};
// Internally it will create Logic& and int& references.
dsml::Sm<MyMachine, Logic, int> sm{logic, num};
sm.process_event("evt1"_e);
std::cout << num << '\n';
}
If your event base type is a complete type then you can also pass a value of that type to the state machine. E.g.:
// Base type is "int".
static constexpr auto evt = dsml::Event<int>{};
struct MyMachine
{
auto operator()() const noexcept
{
using namespace dsml::literals;
using namespace dsml::operators;
// Guard will accept value of "int".
const auto guard = [](int x){ return x <= 5; };
return dsml::make_transition_table(
dsml::initial_state + evt [ guard ] = "A"_s
, dsml::initial_state + evt [ ! guard ] = "B"_s
);
}
};
void func()
{
dsml::Sm<MyMachine> sm{};
// Value 3 will be passed to the guard/action.
// The event has an call operator which will accept that value.
sm.process_event(evt(3));
}
struct OtherMachine
{
auto operator()() const noexcept
{
return dsml::make_transition_table(
dsml::initial_state + "evt1"_e = "B"_s
, dsml::initial_state + "evt2"_e = "C"_s
, "B"_s + "evt1"_e = dsml::final_state
, "C"_s + "evt1"_e = dsml::final_state
);
}
};
struct CompositeMachine
{
auto operator()() const noexcept
{
return dsml::make_transition_table(
dsml::initial_state + "evt1"_e = dsml::State<OtherMachine>{}
, dsml::State<OtherMachine>{} + "evt2"_e = "A"_s
);
}
};
Internally the whole state machine is connected like this:
Transitions to the sub-machine are connected to its initial state and transitions from the sub-machine are connected to its final state.
Useful e.g. for logging.
// must be inherited from dsml::Observer
struct MyObserver : dsml::Observer
{
// methods are not virtual
template <typename TEvent>
void event()
{
std::cout << "event: " << TEvent::c_str() << '\n';
}
template <typename TGuard>
void guard(const TGuard&, const bool result)
{
std::cout << "guard " << result << '\n';
}
template <typename TAction>
void action(const TAction&)
{
std::cout << "action\n";
}
template <typename TSrcState, typename TDstState>
void state_change()
{
std::cout << TSrcState::c_str()
<< " -> "
<< TDstState::c_str()
<< '\n';
}
};
struct MyMachine
{
auto operator()() const noexcept
{
using namespace dsml::literals;
const auto guard = [](){ return true; };
const auto action = [](){ std::cout << "hello\n"; };
return dsml::make_transition_table(
dsml::initial_state + "evt"_e [ guard ] / action = "A"_s
);
}
};
void func()
{
using namespace dsml::literals;
MyObserver observer{};
dsml::Sm<MyMachine, MyObserver> sm{observer};
sm.process_event("evt"_e);
}
dsml::Sm<MyMachine> sm{};
sm.process_event("evt"_e);
sm.reset();
assert(sm.is(dsml::initial_state));
If an action or a guard throws an exception then the library calls
std::abort()
. State machines don't have exceptions. You have to model it e.g.
with an extra state performing the action and outgoing anonymous transitions with
guards checking if the action failed or not.
return dsml::make_transition_table(
dsml::initial_state + "evt"_e / action = "decision"_s
, "decision"_s [ is_ok ] = "B"_s
, "decision"_s [ ! is_ok ] = "C"_s
);
or
return dsml::make_transition_table(
dsml::initial_state + "evt"_e = "doing"_s
, "doing"_s + dsml::on_entry / action
, "doing"_s [ is_ok ] = "B"_s
, "doing"_s [ ! is_ok ] = "C"_s
);