fsm 0.4.1
Header-only C++20 hierarchical table-driven FSM
Loading...
Searching...
No Matches
fsm::machine< Traits > Class Template Reference

The state machine engine interpreting a Traits' constexpr tables. More...

#include <fsm.hpp>

Public Types

using state_type = typename Traits::State
 
using event_type = typename Traits::Event
 
using context_type = typename Traits::Context
 
using rows_array = std::remove_cvref_t< decltype(Traits::rows)>
 
using specs_array = std::remove_cvref_t< decltype(Traits::states)>
 
using row_type = typename rows_array::value_type
 
using spec_type = typename specs_array::value_type
 
using payload_type = typename row_type::payload_type
 void if none declared
 
using reason_type = typename row_type::reason_type
 
using time_type = typename detail::time_of< Traits >::type
 
using result_type = result< state_type, event_type, reason_type >
 
using observer_type = observer< state_type, event_type, reason_type >
 

Public Member Functions

constexpr machine (context_type &ctx)
 Bind the machine to a caller-owned context. Does not enter any state — call start() with the current time.
 
 machine (const machine &)=delete
 
machineoperator= (const machine &)=delete
 
void start (time_type now)
 Enter the initial state chain (entry actions run, deadlines arm). Idempotent: a second call is a no-op. Entry hooks run under the reentrancy guard: a dispatch() from inside one is refused (refused_reentrant), exactly as during a normal transition.
 
bool post (event_type e)
 Queue an event for delivery after the current dispatch completes (run-to-completion), instead of dispatching immediately.
 
size_t posts_pending () const
 Number of post()ed events still queued (not yet drained).
 
template<event_type E, class Arg >
requires (!std::is_void_v<payload_type> && requires(const Arg& a) { { Traits::make_payload(event_tag<E>{}, a) } -> std::convertible_to<payload_arg>; })
bool post (const Arg &arg)
 Typed post(): same factory-checked construction as the typed dispatch() wrapper, for run-to-completion follow-ups.
 
template<event_type E>
requires (!std::is_void_v<payload_type> && requires { { Traits::make_payload(event_tag<E>{}) } -> std::convertible_to<payload_arg>; })
bool post ()
 Typed post() for events whose factory takes no data.
 
template<class P = payload_type>
requires (!std::is_void_v<P> && std::is_same_v<P, payload_type>)
bool post (event_type e, const P &p)
 post() carrying input data (machines with a Payload).
 
result_type dispatch (event_type e, time_type now)
 Deliver an event and return the verdict.
 
template<class P = payload_type>
requires (!std::is_void_v<P> && std::is_same_v<P, payload_type>)
result_type dispatch (event_type e, const P &p, time_type now)
 Deliver an event carrying input data (machines with a Payload).
 
template<event_type E, class Arg >
requires (!std::is_void_v<payload_type> && requires(const Arg& a) { { Traits::make_payload(event_tag<E>{}, a) } -> std::convertible_to<payload_arg>; })
result_type dispatch (const Arg &arg, time_type now)
 Typed dispatch: the event is a compile-time value, the payload is built by the traits' factory — wrong data for the event is a compile error.
 
template<event_type E>
requires (!std::is_void_v<payload_type> && requires { { Traits::make_payload(event_tag<E>{}) } -> std::convertible_to<payload_arg>; })
result_type dispatch (time_type now)
 Typed dispatch for events whose factory takes no data.
 
bool next_deadline (time_type &out) const
 Earliest armed deadline across the machine's own timeout slots.
 
unsigned service (time_type now)
 Fire every expired deadline, earliest first.
 
state_type state () const
 Current leaf state.
 
bool in (state_type s) const
 True if s is the current leaf or one of its active ancestors.
 
bool started () const
 True once start() has run.
 
void set_observer (const observer_type *o)
 Attach a debug observer (nullptr detaches). Not owned — the observer must outlive its attachment. Compiled out entirely when FSM_ENABLE_OBSERVER is 0.
 
template<class W >
void dump (W &&write) const
 Stream a human-readable description of the machine — the live table, current leaf, and armed deadlines — through write.
 

Static Public Member Functions

template<class F >
static constexpr void for_each_row (F &&f)
 Visit every transition row as f(row, index) (constexpr-capable).
 
template<class F >
static constexpr void for_each_state (F &&f)
 Visit every state spec as f(spec, index) (constexpr-capable).
 
static constexpr void put_reason_name (detail::line_buf &lb, reason_type r)
 Append the (hooked or numeric) name of refusal reason r to lb.
 
static constexpr void put_state_name (detail::line_buf &lb, state_type s)
 Append the (hooked or numeric) name of s to lb.
 
static constexpr void put_event_name (detail::line_buf &lb, event_type e)
 Append the (hooked or numeric) name of e to lb.
 

Static Public Attributes

static constexpr size_t state_count = static_cast<size_t>(state_type::Count)
 
static constexpr size_t event_count = static_cast<size_t>(event_type::Count)
 
static constexpr size_t row_count = Traits::rows.size()
 
static constexpr bool strict = detail::strict_of<Traits>()
 

Detailed Description

template<class Traits>
class fsm::machine< Traits >

The state machine engine interpreting a Traits' constexpr tables.

Template Parameters
Traitsstruct providing: State/Event enum classes (dense, Count sentinel), Context, states (std::array of fsm::state_spec), rows (std::array of fsm::row), initial; optionally Time (default uint32_t), strict (default true), state_name/event_name hooks. Payload and Reason are deduced from the rows' element type.

Instantiation static_asserts the full validator suite; a malformed table is a compile error, not a runtime surprise.

The machine owns no clock: start, dispatch and service take the current monotonic time. Arm, fire and transit therefore all happen on the caller's task — the design assumes (and only supports) single-task use.

struct Traits {
enum class State : uint8_t { Locked, Unlocked, Count };
enum class Event : uint8_t { Coin, Push, Relock, Count };
struct Context { int credit; };
static constexpr auto states = std::to_array<Spec>({
{.state = State::Locked, .parent = fsm::root, .deadline = fsm::no_timeout},
{.state = State::Unlocked, .parent = fsm::root, .deadline = fsm::after(5000, Event::Relock)},
});
static constexpr auto rows = std::to_array<Row>({
{.from = State::Locked, .on = Event::Coin, .to = fsm::to(State::Unlocked)},
{.from = State::Unlocked, .on = Event::Relock, .to = fsm::to(State::Locked)},
{.from = fsm::any, .on = Event::Push, .to = fsm::internal},
{.from = fsm::any, .on = Event::Coin, .to = fsm::internal},
{.from = State::Locked, .on = Event::Relock, .to = fsm::internal},
});
static constexpr State initial = State::Locked;
};
Traits::Context ctx{};
m.start(now_ms());
auto r = m.dispatch(Traits::Event::Coin, now_ms());
The state machine engine interpreting a Traits' constexpr tables.
Definition fsm.hpp:972
constexpr internal_t internal
Row target: handle the event (run the action) without exit/entry.
Definition fsm.hpp:153
constexpr any_t any
Wildcard row source: matches any state, after all ancestor levels.
Definition fsm.hpp:146
constexpr timeout_spec< Event > after(uint32_t ms, Event e)
Declare "dispatch @p e after @p ms milliseconds in this state".
Definition fsm.hpp:371
constexpr no_timeout_t no_timeout
State deadline declaration: this state has no timeout (explicitly).
Definition fsm.hpp:160
constexpr root_t root
Parent declaration: this state is a hierarchy root (no parent).
Definition fsm.hpp:295
One transition-table row: {from, on, when, then, to}.
Definition fsm.hpp:416
One state's specification: hierarchy links, actions, deadline.
Definition fsm.hpp:441

Member Typedef Documentation

◆ context_type

template<class Traits >
using fsm::machine< Traits >::context_type = typename Traits::Context

◆ event_type

template<class Traits >
using fsm::machine< Traits >::event_type = typename Traits::Event

◆ observer_type

template<class Traits >
using fsm::machine< Traits >::observer_type = observer<state_type, event_type, reason_type>

◆ payload_type

template<class Traits >
using fsm::machine< Traits >::payload_type = typename row_type::payload_type

void if none declared

◆ reason_type

template<class Traits >
using fsm::machine< Traits >::reason_type = typename row_type::reason_type

◆ result_type

template<class Traits >
using fsm::machine< Traits >::result_type = result<state_type, event_type, reason_type>

◆ row_type

template<class Traits >
using fsm::machine< Traits >::row_type = typename rows_array::value_type

◆ rows_array

template<class Traits >
using fsm::machine< Traits >::rows_array = std::remove_cvref_t<decltype(Traits::rows)>

◆ spec_type

template<class Traits >
using fsm::machine< Traits >::spec_type = typename specs_array::value_type

◆ specs_array

template<class Traits >
using fsm::machine< Traits >::specs_array = std::remove_cvref_t<decltype(Traits::states)>

◆ state_type

template<class Traits >
using fsm::machine< Traits >::state_type = typename Traits::State

◆ time_type

template<class Traits >
using fsm::machine< Traits >::time_type = typename detail::time_of<Traits>::type

Constructor & Destructor Documentation

◆ machine() [1/2]

template<class Traits >
constexpr fsm::machine< Traits >::machine ( context_type ctx)
inlineexplicitconstexpr

Bind the machine to a caller-owned context. Does not enter any state — call start() with the current time.

◆ machine() [2/2]

template<class Traits >
fsm::machine< Traits >::machine ( const machine< Traits > &  )
delete

Not copyable: a machine is a unique stateful device bound to one context — a copy would alias the context and duplicate armed deadlines. Construct machines in place.

Member Function Documentation

◆ dispatch() [1/4]

template<class Traits >
template<event_type E, class Arg >
requires (!std::is_void_v<payload_type> && requires(const Arg& a) { { Traits::make_payload(event_tag<E>{}, a) } -> std::convertible_to<payload_arg>; })
result_type fsm::machine< Traits >::dispatch ( const Arg &  arg,
time_type  now 
)
inline

Typed dispatch: the event is a compile-time value, the payload is built by the traits' factory — wrong data for the event is a compile error.

Enabled per event by declaring an overload in the traits:

static constexpr Payload make_payload(fsm::event_tag<Ev::Call>, FloorCall c);
Tag carrying an event as a compile-time value.
Definition fsm.hpp:173

Then m.dispatch<Ev::Call>(FloorCall{.floor = 4}, now) constructs the machine Payload through that factory; passing anything the factory does not accept for Ev::Call fails overload resolution. Rows, guards and actions are unchanged — they still receive the machine-wide Payload.

◆ dispatch() [2/4]

template<class Traits >
template<class P = payload_type>
requires (!std::is_void_v<P> && std::is_same_v<P, payload_type>)
result_type fsm::machine< Traits >::dispatch ( event_type  e,
const P &  p,
time_type  now 
)
inline

Deliver an event carrying input data (machines with a Payload).

The payload is forwarded by const reference to every guard and action evaluated for this event. Mixed per-event input types live inside the Payload as a tagged union / variant. The payload-less dispatch() overload forwards a value-initialized Payload{}, as do timeout-fired events from service().

◆ dispatch() [3/4]

template<class Traits >
result_type fsm::machine< Traits >::dispatch ( event_type  e,
time_type  now 
)
inline

Deliver an event and return the verdict.

Matching walks the active chain leaf → ancestors, then fsm::any rows; within a level, table order; the first row whose guard passes decides. With no deciding row the event is refused: status::refused_guard if something matched but declined, status::unhandled if nothing considered the pair at all.

◆ dispatch() [4/4]

template<class Traits >
template<event_type E>
requires (!std::is_void_v<payload_type> && requires { { Traits::make_payload(event_tag<E>{}) } -> std::convertible_to<payload_arg>; })
result_type fsm::machine< Traits >::dispatch ( time_type  now)
inline

Typed dispatch for events whose factory takes no data.

◆ dump()

template<class Traits >
template<class W >
void fsm::machine< Traits >::dump ( W &&  write) const
inline

Stream a human-readable description of the machine — the live table, current leaf, and armed deadlines — through write.

write is called once per line with a NUL-terminated const char* (no trailing newline). No allocation. Names come from the traits' state_name/event_name hooks when present, else s<N>/e<N>. Useful for describing a machine over a control channel or log.

◆ for_each_row()

template<class Traits >
template<class F >
static constexpr void fsm::machine< Traits >::for_each_row ( F &&  f)
inlinestaticconstexpr

Visit every transition row as f(row, index) (constexpr-capable).

◆ for_each_state()

template<class Traits >
template<class F >
static constexpr void fsm::machine< Traits >::for_each_state ( F &&  f)
inlinestaticconstexpr

Visit every state spec as f(spec, index) (constexpr-capable).

◆ in()

template<class Traits >
bool fsm::machine< Traits >::in ( state_type  s) const
inline

True if s is the current leaf or one of its active ancestors.

◆ next_deadline()

template<class Traits >
bool fsm::machine< Traits >::next_deadline ( time_type out) const
inline

Earliest armed deadline across the machine's own timeout slots.

The machine iterates its own storage, so a caller cannot forget a deadline source when computing how long to block. Typical loop: wait = next_deadline(dl) ? dl - now : forever; block(wait); service(now);

Parameters
[out]outthe earliest deadline, valid when true is returned
Returns
true if at least one deadline is armed

◆ operator=()

template<class Traits >
machine & fsm::machine< Traits >::operator= ( const machine< Traits > &  )
delete

◆ post() [1/4]

template<class Traits >
template<event_type E>
requires (!std::is_void_v<payload_type> && requires { { Traits::make_payload(event_tag<E>{}) } -> std::convertible_to<payload_arg>; })
bool fsm::machine< Traits >::post ( )
inline

Typed post() for events whose factory takes no data.

◆ post() [2/4]

template<class Traits >
template<event_type E, class Arg >
requires (!std::is_void_v<payload_type> && requires(const Arg& a) { { Traits::make_payload(event_tag<E>{}, a) } -> std::convertible_to<payload_arg>; })
bool fsm::machine< Traits >::post ( const Arg &  arg)
inline

Typed post(): same factory-checked construction as the typed dispatch() wrapper, for run-to-completion follow-ups.

◆ post() [3/4]

template<class Traits >
bool fsm::machine< Traits >::post ( event_type  e)
inline

Queue an event for delivery after the current dispatch completes (run-to-completion), instead of dispatching immediately.

Callable from guards, actions and entry/exit hooks — the places where dispatch() is refused as reentrant. Posted events are drained FIFO before the outermost dispatch()/service()/start() call returns to the caller; each drained event goes through the full table with its own verdict (visible to the observer). Called outside any dispatch, the events are delivered on the next dispatch()/service() call.

This is an ordered outbox, not deferred-event semantics: a posted event that no row accepts is refused like any other, never parked for a later state. Bounds: the queue holds Traits::post_queue_depth entries (default 4) — post() returns false (and drops the event) when full or before start(); a drain processes at most 4 * depth events per top-level call as livelock protection (a self-sustaining post cycle is a table bug), leaving any excess queued for the next call.

Returns
true if the event was queued

◆ post() [4/4]

template<class Traits >
template<class P = payload_type>
requires (!std::is_void_v<P> && std::is_same_v<P, payload_type>)
bool fsm::machine< Traits >::post ( event_type  e,
const P &  p 
)
inline

post() carrying input data (machines with a Payload).

◆ posts_pending()

template<class Traits >
size_t fsm::machine< Traits >::posts_pending ( ) const
inline

Number of post()ed events still queued (not yet drained).

Zero after any top-level dispatch()/service()/start() returns, unless the livelock fuse truncated a self-sustaining post chain — which makes this the direct settle check: m.dispatch(e, now); m.posts_pending() == 0 proves the cascade completed within the fuse bound. Also useful as a debug assertion in the event loop.

◆ put_event_name()

template<class Traits >
static constexpr void fsm::machine< Traits >::put_event_name ( detail::line_buf &  lb,
event_type  e 
)
inlinestaticconstexpr

Append the (hooked or numeric) name of e to lb.

◆ put_reason_name()

template<class Traits >
static constexpr void fsm::machine< Traits >::put_reason_name ( detail::line_buf &  lb,
reason_type  r 
)
inlinestaticconstexpr

Append the (hooked or numeric) name of refusal reason r to lb.

◆ put_state_name()

template<class Traits >
static constexpr void fsm::machine< Traits >::put_state_name ( detail::line_buf &  lb,
state_type  s 
)
inlinestaticconstexpr

Append the (hooked or numeric) name of s to lb.

◆ service()

template<class Traits >
unsigned fsm::machine< Traits >::service ( time_type  now)
inline

Fire every expired deadline, earliest first.

Each expired slot is disarmed and its declared event dispatched through the normal table (full verdict, observer, refusal semantics — a refused or unhandled timeout event does not re-fire). Transitions caused by one firing can arm or disarm others; the scan repeats until nothing armed is expired. An iteration fuse of 4 * state_count bounds pathological zero-delay self-loops.

Reentrancy: service() called from inside a guard/action/hook is a no-op returning 0 — the expired slots stay armed and fire on the next top-level service() call (disarming them here would lose their events, since the nested dispatch would be refused as reentrant).

Parameters
nowcurrent monotonic time
Returns
number of deadlines fired

◆ set_observer()

template<class Traits >
void fsm::machine< Traits >::set_observer ( const observer_type o)
inline

Attach a debug observer (nullptr detaches). Not owned — the observer must outlive its attachment. Compiled out entirely when FSM_ENABLE_OBSERVER is 0.

◆ start()

template<class Traits >
void fsm::machine< Traits >::start ( time_type  now)
inline

Enter the initial state chain (entry actions run, deadlines arm). Idempotent: a second call is a no-op. Entry hooks run under the reentrancy guard: a dispatch() from inside one is refused (refused_reentrant), exactly as during a normal transition.

Parameters
nowcurrent monotonic time

◆ started()

template<class Traits >
bool fsm::machine< Traits >::started ( ) const
inline

True once start() has run.

◆ state()

template<class Traits >
state_type fsm::machine< Traits >::state ( ) const
inline

Current leaf state.

Member Data Documentation

◆ event_count

template<class Traits >
constexpr size_t fsm::machine< Traits >::event_count = static_cast<size_t>(event_type::Count)
staticconstexpr

◆ row_count

template<class Traits >
constexpr size_t fsm::machine< Traits >::row_count = Traits::rows.size()
staticconstexpr

◆ state_count

template<class Traits >
constexpr size_t fsm::machine< Traits >::state_count = static_cast<size_t>(state_type::Count)
staticconstexpr

◆ strict

template<class Traits >
constexpr bool fsm::machine< Traits >::strict = detail::strict_of<Traits>()
staticconstexpr

The documentation for this class was generated from the following file: