feat(backmp11): observer API

This commit is contained in:
Christian Granzin
2026-06-08 18:56:14 +02:00
committed by Christian Granzin
parent cd3fba62a6
commit 2ff2b5caea
20 changed files with 944 additions and 44 deletions
@@ -0,0 +1,226 @@
// Copyright 2026 Christian Granzin
// Copyright 2010 Christophe Henry
// henry UNDERSCORE christophe AT hotmail DOT com
// This is an extended version of the state machine available in the boost::mpl library
// Distributed under the same license as the original.
// Copyright for the original version:
// Copyright 2005 David Abrahams and Aleksey Gurtovoy. Distributed
// under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
#include <string>
#include <iostream>
#include <boost/core/typeinfo.hpp>
#include <boost/msm/backmp11/state_machine.hpp>
#include <boost/msm/backmp11/observer.hpp>
#include <boost/msm/front/functor_row.hpp>
#include <boost/msm/front/state_machine_def.hpp>
namespace back = boost::msm::backmp11;
namespace front = boost::msm::front;
using front::none;
using front::Row;
using front::Internal;
namespace mp11 = boost::mp11;
namespace
{
// Logger for state machine activities.
// Logs the beginning of event processing and the result of each processed transition.
class Logger : public back::default_observer
{
public:
template <typename StateMachine, typename Event>
void pre_process_event(const StateMachine& sm, const Event& event)
{
log(to_string(sm) + " processing event " + to_string(event));
}
template <typename Source, typename Event, typename Target, typename Action,
typename Guard, typename StateMachine>
void post_process_transition(const StateMachine& sm, size_t /*region_id*/,
process_result result)
{
log(to_string(sm) + " processed transition \"" + to_string<Source>() +
" + " + to_string<Event>() + guard_to_string<Guard>() +
action_to_string<Action>() + target_to_string<Target>() + "\" (" +
to_string(result) + ")");
}
template <typename Event, typename Action, typename Guard,
typename StateMachine>
void post_process_transition(const StateMachine& sm, process_result result)
{
log(to_string(sm) + " processed transition \"" + to_string(sm) + " + " +
to_string<Event>() + guard_to_string<Guard>() +
action_to_string<Action>() + "\" (" + to_string(result) + ")");
}
protected:
virtual void log(std::string_view msg)
{
std::cout << msg << std::endl;
}
private:
static std::string to_string(const std::type_info& type)
{
auto full_name = boost::core::demangled_name(type);
return full_name.substr(full_name.rfind(':') + 1);
}
template <typename T>
static std::string to_string()
{
return to_string(typeid(T));
}
template <typename FrontEnd, typename Config, typename Derived>
static std::string to_string(const back::state_machine<FrontEnd, Config, Derived>& /*sm*/)
{
return to_string<FrontEnd>();
}
template <typename T>
static std::string to_string(const T& /*event*/)
{
return to_string<T>();
}
static std::string to_string(const std::any& event)
{
return to_string(event.type());
}
template <typename T>
static std::string target_to_string()
{
if constexpr (!std::is_same_v<T, front::none>)
{
return std::string{" -> "} + to_string<T>();
}
return {};
}
template <typename T>
static std::string action_to_string()
{
if constexpr (!std::is_same_v<T, front::none>)
{
return std::string{" / "} + to_string<T>();
}
return {};
}
template <typename T>
static std::string guard_to_string()
{
if constexpr (!std::is_same_v<T, front::none>)
{
return std::string{" [ "} + to_string<T>() + " ]";
}
return {};
}
static std::string to_string(process_result result)
{
using Enum = boost::msm::back::HandledEnum;
switch (result)
{
case Enum::HANDLED_FALSE:
return "discarded";
case Enum::HANDLED_TRUE:
return "handled";
case Enum::HANDLED_GUARD_REJECT:
return "rejected";
case Enum::HANDLED_DEFERRED:
return "deferred";
default:
return {};
}
}
};
// Events.
struct TransitionEvent {};
struct InternalTransitionEvent {};
struct SmInternalTransitionEvent {};
// Actions
struct MyAction
{
template<typename Fsm>
void operator()(Fsm&)
{
}
};
// Guards
struct MyGuard
{
template<typename Fsm>
bool operator()(Fsm&) const
{
return true;
}
};
struct NotMyGuard
{
template<typename Fsm>
bool operator()(Fsm&) const
{
return false;
}
};
// States.
struct MyState : front::state<> {};
struct MyOtherState : front::state<> {};
// State machine.
// We leave out the trailing underscore for prettier logs.
// You could also implement `to_string()` methods to explicitly set names and
// use these in the logger implementation.
struct MyStateMachine : front::state_machine_def<MyStateMachine>
{
using initial_state = MyState;
using transition_table = mp11::mp_list<
Row<MyState , InternalTransitionEvent, none , MyAction, none>,
Row<MyState , TransitionEvent , MyOtherState, MyAction, MyGuard>,
Row<MyState , TransitionEvent , MyOtherState, MyAction, NotMyGuard>,
Row<MyOtherState, TransitionEvent , MyState , none , none>
>;
using internal_transition_table = mp11::mp_list<
Internal<SmInternalTransitionEvent, MyAction, none>
>;
};
struct MyConfig : back::state_machine_config
{
using observer = Logger;
};
using MyStateMachineBe = back::state_machine<MyStateMachine, MyConfig>;
[[maybe_unused]] void logger_example()
{
MyStateMachineBe test_machine;
std::cout << "Starting state machine..." << std::endl;
test_machine.start();
test_machine.process_event(TransitionEvent{});
test_machine.process_event(InternalTransitionEvent{});
test_machine.process_event(TransitionEvent{});
test_machine.process_event(SmInternalTransitionEvent{});
std::cout << "Stopping state machine..." << std::endl;
test_machine.stop();
}
}
+27 -5
View File
@@ -16,8 +16,8 @@ It offers a significant improvement in runtime and memory usage, as can be seen
| back_favor_compile_time | 12 | 821 | 241 | 1.0
| back11 | 26 | 2675 | 92 | 0.7
| backmp11 | 2 | 236 | 18 | 0.2
| backmp11_favor_compile_time | 2 | 225 | 44 | 2.2
| sml | 3 | 242 | 57 | 0.1
| backmp11_favor_compile_time | 2 | 226 | 44 | 2.2
| sml | 4 | 254 | 64 | 0.1
|=======================================================================================================
@@ -28,9 +28,9 @@ It offers a significant improvement in runtime and memory usage, as can be seen
| | Compile time / sec | Compile RAM / MB | Binary size / kB | Runtime / sec
| back | 32 | 2160 | 252 | 3.7
| back_favor_compile_time | 37 | 1747 | 974 | 263
| backmp11 | 5 | 360 | 55 | 0.9
| backmp11_favor_compile_time | 3 | 263 | 96 | 7.5
| sml | 12 | 567 | 436 | 2.5
| backmp11 | 5 | 362 | 55 | 0.9
| backmp11_favor_compile_time | 3 | 266 | 96 | 7.5
| sml | 13 | 612 | 460 | 2.8
|=======================================================================================================
@@ -68,6 +68,8 @@ struct default_state_machine_config
using event_pool_container = std::deque<T>;
// Type of the Fsm parameter passed in actions and guards.
using fsm_parameter = local_transition_owner;
// Sets up an observer for monitoring state machine activities.
using observer = no_observer;
// Identifies the uppermost machine in hierarchical state machines.
using root_sm = no_root_sm;
};
@@ -117,6 +119,26 @@ If a context is configured, a reference to an instance of it must be passed as t
In hierarchical state machines the root state machine has to be set as well.
=== Observer
The setting `observer` sets up an xref:reference:boost/msm/backmp11/default_observer.adoc[`observer member`] in the state machine for monitoring state machine activities.
An observer is particularly useful for logging and capturing metrics. You can find a demo logger implementation in the xref:backmp11-back-end/examples.adoc#_logger[`Logger example`].
If `using observer = MyObserver;` is defined in the config, the following API becomes available to access it in the state machine:
```cpp
MyObserver& state_machine::get_observer();
```
This creates an observer instance whose lifetime is managed within the state machine. If your instance's lifetime is managed externally, wrap your observer into an `observer_ref`:
```cpp
using observer = observer_ref<MyObserver>;
```
If an observer ref is configured, a reference to an instance of it must be passed as the first argument (second argument if a context is also configured) to the `state_machine` constructor. In hierarchical state machines the root state machine has to be set as well.
=== `Fsm` parameter of actions and guards
The setting `fsm_parameter` defines the instance of the `Fsm& fsm` parameter that is passed to actions and guards in hierarchical state machines.
@@ -0,0 +1,23 @@
= Examples
== xref:attachment$backmp11/Logger.cpp[Logger]
This example contains an observer implementation for logging state machine activities.
The implementation logs the start of event processing and every processed transition.
Running the example produces the following output:
```bash
Starting state machine...
MyStateMachine processed transition "none + starting -> MyState" (handled)
MyStateMachine processing event TransitionEvent
MyStateMachine processed transition "MyState + TransitionEvent [ NotMyGuard ] / MyAction -> MyOtherState" (rejected)
MyStateMachine processed transition "MyState + TransitionEvent [ MyGuard ] / MyAction -> MyOtherState" (handled)
MyStateMachine processing event TransitionEvent
MyStateMachine processed transition "MyOtherState + TransitionEvent -> MyState" (handled)
MyStateMachine processing event InternalTransitionEvent
MyStateMachine processed transition "MyState + InternalTransitionEvent / MyAction" (handled)
MyStateMachine processing event SmInternalTransitionEvent
MyStateMachine processed transition "MyStateMachine + SmInternalTransitionEvent / MyAction" (handled)
Stopping state machine...
```
@@ -5,6 +5,7 @@
== Boost 1.92
* New features (`backmp11`):
** Provide an observer API for logging support (https://github.com/boostorg/msm/issues/220[#220])
** Forward Kleene events to actions and guards without conversion (https://github.com/boostorg/msm/issues/229[#229])
** Ensure basic exception guarantee and propagate exceptions to the caller (https://github.com/boostorg/msm/issues/221[#221])
** Provide a reflection API and serialization support for Boost.Serialization, Boost.JSON and nlohmann/json (https://github.com/boostorg/msm/issues/197[#197])
+1
View File
@@ -11,6 +11,7 @@
*** xref:back-back-end/examples.adoc[Examples]
*** xref:back-back-end/performance-compilers.adoc[Performance / Compilers]
** xref:backmp11-back-end.adoc[`backmp11` back-end (C++17, experimental)]
*** xref:backmp11-back-end/examples.adoc[Examples]
** xref:internals.adoc[Internals]
** xref:acknowledgements.adoc[Acknowledgements]
** xref:version-history.adoc[Version history]
+1
View File
@@ -1 +1,2 @@
#include <boost/msm/backmp11/state_machine.hpp>
#include <boost/msm/backmp11/observer.hpp>
@@ -94,7 +94,7 @@ class deferred_event : public event_occurrence
return std::nullopt;
}
mark_for_deletion();
return sm.process_event_internal(m_event, process_info::event_pool);
return sm.process_event_observed(m_event, process_info::event_pool);
}
private:
@@ -262,7 +262,6 @@ struct compile_policy_impl<
struct forward_transition
{
using current_state_type = Submachine;
using next_state_type = Submachine;
using transition_event = Event;
static process_result process(StateMachine& sm,
@@ -275,7 +274,7 @@ struct compile_policy_impl<
process_info::submachine_call;
process_result result =
sm.template get_state<Submachine>()
.process_event_internal(event, info);
.process_event_observed(event, info);
return result;
}
};
@@ -113,14 +113,6 @@ class context_member
context_member(Context& context) noexcept : m_context(&context) {}
context_member(const context_member&) noexcept = default;
context_member& operator=(const context_member&) noexcept = default;
context_member(context_member&&) noexcept = default;
context_member& operator=(context_member&&) noexcept = default;
private:
template <typename, typename, typename>
friend class backmp11::state_machine;
@@ -151,16 +143,60 @@ class context_member<no_context, nesting_role::unknown>
static constexpr bool has_context_member = false;
};
template <typename Observer, nesting_role NestingRole>
class observer_member
{
protected:
static constexpr bool has_observer_member = true;
observer_member() = default;
template <typename T,
typename = std::enable_if_t<std::is_constructible_v<Observer, T>>>
observer_member(T&& arg) : m_observer(std::forward<T>(arg)) {}
private:
template <typename, typename, typename>
friend class backmp11::state_machine;
template <typename, nesting_role>
friend class state_machine_base;
Observer m_observer;
};
template <typename Observer>
class observer_member<Observer, nesting_role::nested>
{
protected:
static constexpr bool has_observer_member = false;
};
template <>
class observer_member<no_observer, nesting_role::root>
{
protected:
static constexpr bool has_observer_member = false;
};
template <>
class observer_member<no_observer, nesting_role::unknown>
{
protected:
static constexpr bool has_observer_member = false;
};
template <typename Config, nesting_role NestingRole>
class state_machine_base
: public event_pool_processor<Config::template event_pool_container,
basic_polymorphic<event_occurrence>>,
public context_member<typename Config::context, NestingRole>
public context_member<typename Config::context, NestingRole>,
public observer_member<typename Config::observer, NestingRole>
{
public:
using config_t = Config;
using root_sm_t = typename Config::root_sm;
using context_t = typename Config::context;
using observer_t = typename Config::observer;
/// Gets the context of the state machine.
/// See @ref state_machine_config::context.
@@ -187,6 +223,31 @@ class state_machine_base
}
}
/// Get the observer of the state machine.
/// See @ref state_machine_config::observer.
template <bool C = !std::is_same_v<observer_t, no_observer>,
typename = std::enable_if_t<C>>
observer_t& get_observer()
{
return const_cast<observer_t&>(std::as_const(*this).get_observer());
}
/// Gets the observer of the state machine.
/// See @ref state_machine_config::observer.
template <bool C = !std::is_same_v<observer_t, no_observer>,
typename = std::enable_if_t<C>>
const observer_t& get_observer() const
{
if constexpr (NestingRole == nesting_role::root)
{
return this->m_observer;
}
else
{
return (*m_root_sm)->m_observer;
}
}
/**
* @brief Processes up to `max_events` from the event pool.
*
@@ -231,9 +292,24 @@ class state_machine_base
protected:
using context_member =
detail::context_member<typename Config::context, NestingRole>;
using observer_member =
detail::observer_member<typename Config::observer, NestingRole>;
using context_member::context_member;
using observer_member::observer_member;
state_machine_base() = default;
template <typename T, typename = std::enable_if_t<
context_member::has_context_member &&
observer_member::has_observer_member &&
std::is_constructible_v<observer_t, T>>>
state_machine_base(context_t& context, T&& arg)
: context_member(context), observer_member(std::forward<T>(arg))
{
}
machine_state get_machine_state() const
{
return m_machine_state;
@@ -247,6 +247,33 @@ struct transition_table_impl
static process_result process(StateMachine& sm,
uint8_t region_id,
const Event& event)
{
if constexpr (!std::is_same_v<typename StateMachine::observer_t,
no_observer>)
{
sm.get_observer()
.template pre_process_transition<
typename Row::Source, typename Row::Evt,
typename Row::Target, typename Row::Action,
typename Row::Guard>(sm, region_id);
}
const auto result = process_impl(sm, region_id, event);
if constexpr (!std::is_same_v<typename StateMachine::observer_t,
no_observer>)
{
sm.get_observer()
.template post_process_transition<
typename Row::Source, typename Row::Evt,
typename Row::Target, typename Row::Action,
typename Row::Guard>(sm, region_id, result);
}
return result;
}
template <typename Event>
static process_result process_impl(StateMachine& sm,
uint8_t region_id,
const Event& event)
{
auto& state_id = sm.m_active_state_ids[region_id];
[[maybe_unused]] constexpr auto current_state_id =
@@ -309,12 +336,38 @@ struct transition_table_impl
{
using transition_event = typename Row::Evt;
using current_state_type = State;
using next_state_type = current_state_type;
template <typename Event>
static process_result process(StateMachine& sm,
uint8_t region_id,
const Event& event)
{
if constexpr (!std::is_same_v<typename StateMachine::observer_t,
no_observer>)
{
sm.get_observer()
.template pre_process_transition<
typename Row::Source, typename Row::Evt,
typename front::none, typename Row::Action,
typename Row::Guard>(sm, region_id);
}
const auto result = process_impl(sm, region_id, event);
if constexpr (!std::is_same_v<typename StateMachine::observer_t,
no_observer>)
{
sm.get_observer()
.template post_process_transition<
typename Row::Source, typename Row::Evt,
typename front::none, typename Row::Action,
typename Row::Guard>(sm, region_id, result);
}
return result;
}
template <typename Event>
static process_result process_impl(StateMachine& sm,
uint8_t region_id,
const Event& event)
{
[[maybe_unused]] const auto state_id = sm.m_active_state_ids[region_id];
BOOST_ASSERT(
@@ -342,6 +395,32 @@ struct transition_table_impl
template <typename Event>
static process_result process(StateMachine& sm,
const Event& event)
{
if constexpr (!std::is_same_v<typename StateMachine::observer_t,
no_observer>)
{
sm.get_observer()
.template pre_process_transition<typename Row::Evt,
typename Row::Action,
typename Row::Guard>(
sm);
}
const auto result = process_impl(sm, event);
if constexpr (!std::is_same_v<typename StateMachine::observer_t,
no_observer>)
{
sm.get_observer()
.template post_process_transition<typename Row::Evt,
typename Row::Action,
typename Row::Guard>(
sm, result);
}
return result;
}
template <typename Event>
static process_result process_impl(StateMachine& sm,
const Event& event)
{
auto& source = sm;
auto& target = source;
@@ -392,7 +392,7 @@ struct compile_policy_impl<favor_compile_time>
static process_result call_process_event(StateMachine& sm, const any_event& event)
{
return sm.template get_state<Submachine&>()
.process_event_internal(event, process_info::submachine_call);
.process_event_observed(event, process_info::submachine_call);
}
std::unordered_map<std::type_index, transition_chain> m_transition_chains;
+153
View File
@@ -0,0 +1,153 @@
// Copyright 2026 Christian Granzin
// Copyright 2010 Christophe Henry
// henry UNDERSCORE christophe AT hotmail DOT com
// This is an extended version of the state machine available in the boost::mpl library
// Distributed under the same license as the original.
// Copyright for the original version:
// Copyright 2005 David Abrahams and Aleksey Gurtovoy. Distributed
// under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
#ifndef BOOST_MSM_BACKMP11_INTERCEPTOR_HPP
#define BOOST_MSM_BACKMP11_INTERCEPTOR_HPP
#include <cstddef>
#include <boost/msm/backmp11/common_types.hpp>
namespace boost::msm::backmp11
{
/// Default observer for state machine activities.
/// Defines no-op hooks for all observable activities.
class default_observer
{
public:
using process_result = backmp11::process_result;
/// Hook called before a (sub-)machine processes an event.
template <typename StateMachine, typename Event>
constexpr void pre_process_event(const StateMachine&, const Event&) const
{
}
/// Hook called after a (sub-)machine processes an event.
template <typename StateMachine, typename Event>
constexpr void post_process_event(const StateMachine&, const Event&,
process_result) const
{
}
/// Hook called before a (sub-)machine processes a transition.
template <typename Source, typename Event, typename Target, typename Action,
typename Guard, typename StateMachine>
constexpr void pre_process_transition(const StateMachine&,
size_t /*region_id*/) const
{
}
/// Hook called after a (sub-)machine processes a transition.
template <typename Source, typename Event, typename Target, typename Action,
typename Guard, typename StateMachine>
constexpr void post_process_transition(const StateMachine&,
size_t /*region_id*/,
process_result) const
{
}
/// Hook called before a (sub-)machine processes a sm-internal transition.
template <typename Event, typename Action, typename Guard,
typename StateMachine>
constexpr void pre_process_transition(const StateMachine&) const
{
}
/// Hook called after a (sub-)machine processes a sm-internal transition.
template <typename Event, typename Action, typename Guard,
typename StateMachine>
constexpr void post_process_transition(const StateMachine&,
process_result) const
{
}
};
/// Wrapper for an observer reference whose lifetime is managed externally.
template <typename Observer>
class observer_ref
{
public:
observer_ref(Observer& observer) noexcept : m_observer(&observer) {}
Observer& get() const noexcept
{
return *m_observer;
}
operator Observer&() const noexcept
{
return *m_observer;
}
/// @copydoc observer::pre_process_event
template <typename StateMachine, typename Event>
void pre_process_event(StateMachine& sm, const Event& event) const
{
m_observer->pre_process_event(sm, event);
}
/// @copydoc observer::post_process_event
template <typename StateMachine, typename Event>
void post_process_event(StateMachine& sm, const Event& event,
process_result result) const
{
m_observer->post_process_event(sm, event, result);
}
/// @copydoc observer::pre_process_transition
template <typename Source, typename Event, typename Target, typename Action,
typename Guard, typename StateMachine>
void pre_process_transition(StateMachine& sm,
size_t region_id) const
{
m_observer->template pre_process_transition<Source, Event, Target,
Action, Guard>(sm,
region_id);
}
/// @copydoc observer::post_process_transition
template <typename Source, typename Event, typename Target, typename Action,
typename Guard, typename StateMachine>
void post_process_transition(StateMachine& sm, size_t region_id,
process_result result) const
{
m_observer->template post_process_transition<Source, Event, Target,
Action, Guard>(
sm, region_id, result);
}
/// @copydoc observer::pre_process_transition
template <typename Event, typename Action, typename Guard,
typename StateMachine>
void pre_process_transition(StateMachine& sm) const
{
m_observer->template pre_process_transition<Event, Action, Guard>(sm);
}
/// @copydoc observer::post_process_transition
template <typename Event, typename Action, typename Guard,
typename StateMachine>
void post_process_transition(StateMachine& sm,
process_result result) const
{
m_observer->template post_process_transition<Event, Action, Guard>(
sm, result);
}
private:
Observer* m_observer;
};
} // namespace boost::msm::backmp11
#endif // BOOST_MSM_BACKMP11_INTERCEPTOR_HPP
+36 -5
View File
@@ -86,6 +86,8 @@ class state_machine
using config_t = typename state_machine_base::config_t;
/// Type of the context (see @ref state_machine_config::context).
using context_t = typename state_machine_base::context_t;
/// Type of the context (see @ref state_machine_config::observer).
using observer_t = typename state_machine_base::observer_t;
/// Type of the root machine (see @ref state_machine_config::root_sm).
using root_sm_t = typename state_machine_base::root_sm_t;
/// Type of the derived machine (corresponds to Derived).
@@ -247,6 +249,7 @@ class state_machine
*
* Requires constructor arguments as configured:
* - Context& (if context = Context is set)
* - Observer& (if observer = observer_ref<Observer> is set)
*/
template <typename... Args>
state_machine(Args&&... args)
@@ -264,10 +267,8 @@ class state_machine
}
state_machine(state_machine& rhs)
: state_machine_base(rhs), m_states(rhs.m_states),
m_active_state_ids(rhs.m_active_state_ids)
: state_machine(static_cast<state_machine const&>(rhs))
{
init();
}
// Copy assignment operator.
@@ -343,7 +344,7 @@ class state_machine
template<class Event>
process_result process_event(Event const& event)
{
return process_event_internal(
return process_event_observed(
compile_policy_impl::normalize_event(event),
detail::process_info::direct_call);
}
@@ -527,7 +528,23 @@ class state_machine
private:
// Main function used internally to process events.
template <class Event>
process_result process_event_internal(Event const& event, detail::process_info info)
process_result process_event_observed(Event const& event,
detail::process_info info)
{
if constexpr (!std::is_same_v<observer_t, no_observer>)
{
this->get_observer().pre_process_event(self(), event);
}
const auto result = process_event_impl(event, info);
if constexpr (!std::is_same_v<observer_t, no_observer>)
{
this->get_observer().post_process_event(self(), event, result);
}
return result;
}
template <class Event>
process_result process_event_impl(Event const& event, detail::process_info info)
{
if (this->m_machine_state != detail::machine_state::idle)
{
@@ -726,8 +743,22 @@ class state_machine
template <typename State>
void operator()(State& state)
{
if constexpr (!std::is_same_v<observer_t, no_observer>)
{
m_self.get_observer()
.template pre_process_transition<front::none, Event, State,
front::none, front::none>(
m_self, m_region_id);
}
state.on_entry(m_event, m_self.get_fsm_argument());
m_self.template on_state_entry_completed<State>(m_region_id++);
if constexpr (!std::is_same_v<observer_t, no_observer>)
{
m_self.get_observer()
.template post_process_transition<front::none, Event, State,
front::none, front::none>(
m_self, m_region_id, process_result::HANDLED_TRUE);
}
}
private:
@@ -91,6 +91,9 @@ struct local_transition_owner {};
template <typename T>
struct no_event_pool_container {};
/// No observer is configured (see @ref state_machine_config::observer).
struct no_observer {};
/// Default state machine configuration.
struct default_state_machine_config
{
@@ -119,6 +122,8 @@ struct default_state_machine_config
/// Type of the Fsm parameter passed in actions and guards.
/// Defaults to @ref local_transition_owner.
using fsm_parameter = local_transition_owner;
/// Sets up an observer for monitoring state machine activities.
using observer = no_observer;
/// Identifies the upper-most machine in hierarchical state machines.
/// Defaults to @ref no_root_sm.
using root_sm = no_root_sm;
+36 -10
View File
@@ -9,14 +9,16 @@
// file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
// back-end
#include "BackCommon.hpp"
//front-end
#include "FrontCommon.hpp"
#ifndef BOOST_MSM_NONSTANDALONE_TEST
#define BOOST_TEST_MODULE test_constructor
#endif
#include <boost/test/unit_test.hpp>
// back-end
#include "BackCommon.hpp"
#include <boost/msm/backmp11/observer.hpp>
//front-end
#include "FrontCommon.hpp"
#include <boost/config.hpp>
namespace msm = boost::msm;
@@ -33,12 +35,6 @@ struct MyState : test::StateBase {};
struct StateMachine_ : test::StateMachineBase_<StateMachine_>
{
using initial_state = MyState;
StateMachine_() = default;
StateMachine_(int n) : number(n) {}
int number{};
};
struct Context
@@ -51,6 +47,19 @@ struct context_config : state_machine_config
using context = Context;
};
using Observer = msm::backmp11::default_observer;
struct observer_config : state_machine_config
{
using observer = Observer;
};
struct co_config : context_config
{
using observer = Observer;
};
BOOST_AUTO_TEST_CASE(context_constructors)
{
using StateMachine = state_machine<StateMachine_, context_config>;
@@ -59,4 +68,21 @@ BOOST_AUTO_TEST_CASE(context_constructors)
StateMachine sm{context};
}
BOOST_AUTO_TEST_CASE(observer_constructors)
{
using StateMachine = state_machine<StateMachine_, observer_config>;
Observer observer;
StateMachine sm{observer};
}
BOOST_AUTO_TEST_CASE(co_constructors)
{
using StateMachine = state_machine<StateMachine_, co_config>;
Context context;
Observer observer;
StateMachine sm{context, observer};
}
} // namespace
+20 -8
View File
@@ -16,6 +16,7 @@
// back-end
#include "Backmp11.hpp"
#include <boost/msm/backmp11/observer.hpp>
//front-end
#include "FrontCommon.hpp"
@@ -66,9 +67,11 @@ struct MyState : public test::StateBase
struct Context
{
int foo{};
int foo{42};
};
using Observer = msm::backmp11::default_observer;
template <typename Config = default_state_machine_config>
struct hierarchical_state_machine
{
@@ -81,6 +84,7 @@ struct ConfigWithRootSm : Config
{
using root_sm = StateMachine;
using context = Context;
using observer = msm::backmp11::observer_ref<Observer>;
};
struct Submachine_ : public test::StateMachineBase_<Submachine_>
@@ -108,7 +112,8 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(copy_operators, StateMachine, TestMachines)
{
using TestMachine = typename StateMachine::StateMachine;
Context context;
TestMachine test_machine{context};
Observer observer;
TestMachine test_machine{context, observer};
test_machine.start();
@@ -117,6 +122,7 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(copy_operators, StateMachine, TestMachines)
{
TestMachine other_test_machine{test_machine};
BOOST_REQUIRE(other_test_machine.get_context().foo == 42);
auto& other_submachine = other_test_machine.template get_state<typename StateMachine::Submachine>();
BOOST_REQUIRE(other_test_machine.template is_state_active<typename StateMachine::Submachine>());
BOOST_REQUIRE(&test_machine.get_root_sm() != &other_test_machine.get_root_sm());
@@ -126,8 +132,9 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(copy_operators, StateMachine, TestMachines)
}
{
TestMachine other_test_machine{context};
TestMachine other_test_machine{context, observer};
other_test_machine = test_machine;
BOOST_REQUIRE(other_test_machine.get_context().foo == 42);
auto& other_submachine = other_test_machine.template get_state<typename StateMachine::Submachine>();
BOOST_REQUIRE(other_test_machine.template is_state_active<typename StateMachine::Submachine>());
BOOST_REQUIRE(&test_machine.get_root_sm() != &other_test_machine.get_root_sm());
@@ -143,7 +150,8 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(move_operators, StateMachine, TestMachines)
{
using TestMachine = typename StateMachine::StateMachine;
Context context;
TestMachine test_machine{context};
Observer observer;
TestMachine test_machine{context, observer};
test_machine.start();
@@ -152,6 +160,7 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(move_operators, StateMachine, TestMachines)
{
TestMachine other_test_machine{std::move(test_machine)};
BOOST_REQUIRE(other_test_machine.get_context().foo == 42);
auto& other_submachine = other_test_machine.template get_state<typename StateMachine::Submachine>();
BOOST_REQUIRE(other_test_machine.template is_state_active<typename StateMachine::Submachine>());
BOOST_REQUIRE(&test_machine.get_root_sm() != &other_test_machine.get_root_sm());
@@ -161,8 +170,9 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(move_operators, StateMachine, TestMachines)
}
{
TestMachine other_test_machine{context};
TestMachine other_test_machine{context, observer};
other_test_machine = std::move(test_machine);
BOOST_REQUIRE(other_test_machine.get_context().foo == 42);
auto& other_submachine = other_test_machine.template get_state<typename StateMachine::Submachine>();
BOOST_REQUIRE(other_test_machine.template is_state_active<typename StateMachine::Submachine>());
BOOST_REQUIRE(&test_machine.get_root_sm() != &other_test_machine.get_root_sm());
@@ -178,14 +188,15 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(copy_event_pool, StateMachine, TestMachines)
{
using TestMachine = typename StateMachine::StateMachine;
Context context;
TestMachine test_machine{context};
Observer observer;
TestMachine test_machine{context, observer};
test_machine.start();
test_machine.enqueue_event(EnterSubmachine{});
{
TestMachine other_test_machine{context};
TestMachine other_test_machine{context, observer};
other_test_machine = test_machine;
BOOST_REQUIRE(other_test_machine.process_event_pool() == 1);
BOOST_REQUIRE(other_test_machine.template is_state_active<typename StateMachine::Submachine>());
@@ -201,7 +212,8 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(move_event_pool, StateMachine, TestMachines)
{
using TestMachine = typename StateMachine::StateMachine;
Context context;
TestMachine test_machine{context};
Observer observer;
TestMachine test_machine{context, observer};
test_machine.start();
+242
View File
@@ -0,0 +1,242 @@
// Copyright 2026 Christian Granzin
// Copyright 2010 Christophe Henry
// henry UNDERSCORE christophe AT hotmail DOT com
// This is an extended version of the state machine available in the boost::mpl library
// Distributed under the same license as the original.
// Copyright for the original version:
// Copyright 2005 David Abrahams and Aleksey Gurtovoy. Distributed
// under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
#ifndef BOOST_MSM_NONSTANDALONE_TEST
#define BOOST_TEST_MODULE backmp11_observer_test
#endif
#include <boost/test/unit_test.hpp>
#include <boost/msm/backmp11/state_machine.hpp>
#include <boost/msm/backmp11/favor_compile_time.hpp>
#include "FrontCommon.hpp"
#include "Utils.hpp"
#include "attachments/backmp11/Logger.cpp"
namespace msm = boost::msm;
namespace mp11 = boost::mp11;
using namespace msm::front;
using namespace msm::backmp11;
namespace
{
class Observer : public Logger
{
public:
template <typename StateMachine, typename Event>
void pre_process_event(const StateMachine& sm, const Event& event)
{
Logger::pre_process_event(sm, event);
pre_process_event_counter++;
}
template <typename StateMachine, typename Event>
void post_process_event(const StateMachine&, const Event&, process_result /*result*/)
{
post_process_event_counter++;
}
template <typename Source, typename Event, typename Target, typename Action,
typename Guard, typename StateMachine>
void pre_process_transition(const StateMachine&, size_t /*region_id*/)
{
pre_process_transition_counter++;
}
template <typename Source, typename Event, typename Target, typename Action,
typename Guard, typename StateMachine>
void post_process_transition(const StateMachine& sm, size_t region_id,
process_result result)
{
Logger::post_process_transition<Source, Event, Target, Action, Guard>(
sm, region_id, result);
post_process_transition_counter++;
}
template <typename Event, typename Action, typename Guard,
typename StateMachine>
void pre_process_transition(const StateMachine&)
{
pre_process_transition_counter++;
}
template <typename Event, typename Action, typename Guard,
typename StateMachine>
void post_process_transition(const StateMachine& sm, process_result result)
{
Logger::post_process_transition<Event, Action, Guard>(sm, result);
post_process_transition_counter++;
}
void log(std::string_view msg) override
{
messages.push_back(std::string{msg});
}
void assert_and_pop_msg(std::string_view msg)
{
BOOST_REQUIRE(messages.front() == msg);
messages.pop_front();
}
std::deque<std::string> messages;
size_t pre_start_counter{};
size_t post_start_counter{};
size_t pre_stop_counter{};
size_t post_stop_counter{};
size_t pre_process_event_counter{};
size_t post_process_event_counter{};
size_t pre_process_transition_counter{};
size_t post_process_transition_counter{};
};
namespace observer_test
{
struct default_config : state_machine_config
{
using observer = Observer;
};
struct favor_compile_time_config : default_config
{
using compile_policy = favor_compile_time;
};
template <typename Config = default_config>
using SmWithObserver = state_machine<MyStateMachine, Config>;
using TestMachines = mp11::mp_list<
SmWithObserver<>,
SmWithObserver<favor_compile_time_config>
>;
BOOST_AUTO_TEST_CASE_TEMPLATE(observer, StateMachine, TestMachines)
{
StateMachine test_machine;
Observer& observer = test_machine.get_observer();
test_machine.start();
observer.assert_and_pop_msg("MyStateMachine processed transition \"none + starting -> MyState\" (handled)");
ASSERT_AND_RESET(observer.pre_process_transition_counter, 1);
ASSERT_AND_RESET(observer.post_process_transition_counter, 1);
test_machine.process_event(TransitionEvent{});
observer.assert_and_pop_msg("MyStateMachine processing event TransitionEvent");
observer.assert_and_pop_msg("MyStateMachine processed transition \"MyState + TransitionEvent [ NotMyGuard ] / MyAction -> MyOtherState\" (rejected)");
observer.assert_and_pop_msg("MyStateMachine processed transition \"MyState + TransitionEvent [ MyGuard ] / MyAction -> MyOtherState\" (handled)");
ASSERT_AND_RESET(observer.pre_process_event_counter, 1);
ASSERT_AND_RESET(observer.pre_process_transition_counter, 2);
ASSERT_AND_RESET(observer.post_process_transition_counter, 2);
ASSERT_AND_RESET(observer.post_process_event_counter, 1);
test_machine.process_event(TransitionEvent{});
observer.assert_and_pop_msg("MyStateMachine processing event TransitionEvent");
observer.assert_and_pop_msg("MyStateMachine processed transition \"MyOtherState + TransitionEvent -> MyState\" (handled)");
ASSERT_AND_RESET(observer.pre_process_event_counter, 1);
ASSERT_AND_RESET(observer.pre_process_transition_counter, 1);
ASSERT_AND_RESET(observer.post_process_transition_counter, 1);
ASSERT_AND_RESET(observer.post_process_event_counter, 1);
test_machine.process_event(InternalTransitionEvent{});
observer.assert_and_pop_msg("MyStateMachine processing event InternalTransitionEvent");
observer.assert_and_pop_msg("MyStateMachine processed transition \"MyState + InternalTransitionEvent / MyAction\" (handled)");
ASSERT_AND_RESET(observer.pre_process_event_counter, 1);
ASSERT_AND_RESET(observer.pre_process_transition_counter, 1);
ASSERT_AND_RESET(observer.post_process_transition_counter, 1);
ASSERT_AND_RESET(observer.post_process_event_counter, 1);
test_machine.process_event(SmInternalTransitionEvent{});
observer.assert_and_pop_msg("MyStateMachine processing event SmInternalTransitionEvent");
observer.assert_and_pop_msg("MyStateMachine processed transition \"MyStateMachine + SmInternalTransitionEvent / MyAction\" (handled)");
ASSERT_AND_RESET(observer.pre_process_event_counter, 1);
ASSERT_AND_RESET(observer.pre_process_transition_counter, 1);
ASSERT_AND_RESET(observer.post_process_transition_counter, 1);
ASSERT_AND_RESET(observer.post_process_event_counter, 1);
test_machine.stop();
}
} // namespace observer
namespace observer_ref_test
{
struct default_config : state_machine_config
{
using observer = observer_ref<Observer>;
};
struct favor_compile_time_config : default_config
{
using compile_policy = favor_compile_time;
};
template <typename Config = default_config>
using SmWithObserver = state_machine<MyStateMachine, Config>;
using TestMachines = mp11::mp_list<
SmWithObserver<>,
SmWithObserver<favor_compile_time_config>
>;
BOOST_AUTO_TEST_CASE_TEMPLATE(observer_ref, StateMachine, TestMachines)
{
Observer observer;
StateMachine test_machine{observer};
test_machine.start();
observer.assert_and_pop_msg("MyStateMachine processed transition \"none + starting -> MyState\" (handled)");
ASSERT_AND_RESET(observer.pre_process_transition_counter, 1);
ASSERT_AND_RESET(observer.post_process_transition_counter, 1);
test_machine.process_event(TransitionEvent{});
observer.assert_and_pop_msg("MyStateMachine processing event TransitionEvent");
observer.assert_and_pop_msg("MyStateMachine processed transition \"MyState + TransitionEvent [ NotMyGuard ] / MyAction -> MyOtherState\" (rejected)");
observer.assert_and_pop_msg("MyStateMachine processed transition \"MyState + TransitionEvent [ MyGuard ] / MyAction -> MyOtherState\" (handled)");
ASSERT_AND_RESET(observer.pre_process_event_counter, 1);
ASSERT_AND_RESET(observer.pre_process_transition_counter, 2);
ASSERT_AND_RESET(observer.post_process_transition_counter, 2);
ASSERT_AND_RESET(observer.post_process_event_counter, 1);
test_machine.process_event(TransitionEvent{});
observer.assert_and_pop_msg("MyStateMachine processing event TransitionEvent");
observer.assert_and_pop_msg("MyStateMachine processed transition \"MyOtherState + TransitionEvent -> MyState\" (handled)");
ASSERT_AND_RESET(observer.pre_process_event_counter, 1);
ASSERT_AND_RESET(observer.pre_process_transition_counter, 1);
ASSERT_AND_RESET(observer.post_process_transition_counter, 1);
ASSERT_AND_RESET(observer.post_process_event_counter, 1);
test_machine.process_event(InternalTransitionEvent{});
observer.assert_and_pop_msg("MyStateMachine processing event InternalTransitionEvent");
observer.assert_and_pop_msg("MyStateMachine processed transition \"MyState + InternalTransitionEvent / MyAction\" (handled)");
ASSERT_AND_RESET(observer.pre_process_event_counter, 1);
ASSERT_AND_RESET(observer.pre_process_transition_counter, 1);
ASSERT_AND_RESET(observer.post_process_transition_counter, 1);
ASSERT_AND_RESET(observer.post_process_event_counter, 1);
test_machine.process_event(SmInternalTransitionEvent{});
observer.assert_and_pop_msg("MyStateMachine processing event SmInternalTransitionEvent");
observer.assert_and_pop_msg("MyStateMachine processed transition \"MyStateMachine + SmInternalTransitionEvent / MyAction\" (handled)");
ASSERT_AND_RESET(observer.pre_process_event_counter, 1);
ASSERT_AND_RESET(observer.pre_process_transition_counter, 1);
ASSERT_AND_RESET(observer.post_process_transition_counter, 1);
ASSERT_AND_RESET(observer.post_process_event_counter, 1);
test_machine.stop();
}
}
} // namespace
+1 -1
View File
@@ -120,7 +120,7 @@ struct DimSwitch_ : front::state_machine_def<DimSwitch_>
visitor.visit_member("brightness", brightness);
}
uint8_t brightness;
uint8_t brightness{};
};
using DimSwitch = backmp11::state_machine<DimSwitch_>;
+2 -1
View File
@@ -7,7 +7,7 @@ if(NOT TARGET tests)
endif()
link_libraries(Boost::msm Boost::unit_test_framework)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/../doc/modules/ROOT)
add_compile_definitions("BOOST_MSM_NONSTANDALONE_TEST")
if(BOOST_MSM_TEST_ONLY_BACKMP11)
add_compile_definitions("BOOST_MSM_TEST_ONLY_BACKMP11")
@@ -88,6 +88,7 @@ add_executable(boost_msm_cxx17_tests
Backmp11FunctorApi.cpp
Backmp11History.cpp
Backmp11ManyDeferTransitions.cpp
Backmp11Observer.cpp
Backmp11RootSm.cpp
Backmp11Serialization.cpp
Backmp11Transitions.cpp
+2
View File
@@ -19,6 +19,7 @@ project
requirements
<library>/boost/msm//boost_msm
<include>.
<include>../doc/modules/ROOT
<toolset>gcc:<cxxflags>"-ftemplate-depth-300 -g0"
<toolset>darwin:<cxxflags>"-ftemplate-depth-300 -g0"
<toolset>intel:<cxxflags>"-g0"
@@ -87,6 +88,7 @@ test-suite msm-unit-tests-cxxstd17
[ run Backmp11FunctorApi.cpp ]
[ run Backmp11History.cpp ]
[ run Backmp11ManyDeferTransitions.cpp ]
[ run Backmp11Observer.cpp ]
[ run Backmp11RootSm.cpp ]
[ run Backmp11Serialization.cpp : : : <define>BOOST_MSM_TEST_NLOHMANN_JSON ]
[ run Backmp11Transitions.cpp ]