From e1acea29a9b08a4501958da0eccd30a9270a276f Mon Sep 17 00:00:00 2001 From: Christian Granzin Date: Fri, 2 Jan 2026 16:22:51 -0500 Subject: [PATCH] feat(backmp11): conditional deferred_events --- .../pages/tutorial/backmp11-back-end.adoc | 25 ++- doc/modules/ROOT/pages/version-history.adoc | 5 + .../backmp11/detail/favor_runtime_speed.hpp | 10 +- .../boost/msm/backmp11/favor_compile_time.hpp | 61 +++++-- .../boost/msm/front/detail/common_states.hpp | 13 +- include/boost/msm/front/puml/puml.hpp | 6 + test/Backmp11Deferred.cpp | 157 ++++++++++++------ 7 files changed, 200 insertions(+), 77 deletions(-) diff --git a/doc/modules/ROOT/pages/tutorial/backmp11-back-end.adoc b/doc/modules/ROOT/pages/tutorial/backmp11-back-end.adoc index fea8fbf7..fd681b60 100644 --- a/doc/modules/ROOT/pages/tutorial/backmp11-back-end.adoc +++ b/doc/modules/ROOT/pages/tutorial/backmp11-back-end.adoc @@ -59,7 +59,7 @@ a| Deferring an event as an action triggered by the same event is not foreseen i *Change:* The public API to defer an event will be changed to `protected`. If needed, users can inherit from `state_machine` and make the API public again, but without guarantees about correct functionality and API consistency. -*Recommendation:* Configure event deferral as a state property instead of using it as a transition action. +*Recommendation:* Configure event deferral as a state property instead of using it as a transition action. Implement conditional deferral with the state's `is_event_deferred` method if needed. | Public access to the event container | 1.90 / 1.91 @@ -259,6 +259,27 @@ A similar friend declaration is available in the `history_impl` classes. **IMPORTANT:** This design allows you to provide any serializer implementation, but due to the need to access private members there is no guarantee that your implementation breaks in a new version of the back-end. +=== Support for conditional event deferral with the `deferred_events` property + +In `back`` & `back11``, the events mentioned in a state's `deferred_events` property are always deferred. +In `backmp11`` they can be deferred conditionally by defining a method `is_event_deferred` for them: + +```cpp +struct MyState : boost::msm::front::state<> +{ + using deferred_events = mp11::mp_list; + + template + bool is_event_deferred(const MyEvent& event, Fsm& fsm) const + { + // Return false or true to decide + // whether the event shall be deferred. + ... + } +}; +``` + + == Resolved issues with respect to `back` === Deferring events in orthogonal regions @@ -400,7 +421,7 @@ Also the `set_states` API is removed. If setting a state is required, this can s ==== The method `get_state_by_id` is removed If you really need to get a state by id, please use the universal visitor API to implement the function on your own. -The backmp11 state_machine has a new method to support getting the id of a state in the visitor: +The `backmp11` `state_machine` has a new method to support getting the id of a state in the visitor: ```cpp template diff --git a/doc/modules/ROOT/pages/version-history.adoc b/doc/modules/ROOT/pages/version-history.adoc index 12aa262f..69df6ecf 100644 --- a/doc/modules/ROOT/pages/version-history.adoc +++ b/doc/modules/ROOT/pages/version-history.adoc @@ -2,6 +2,11 @@ = Version history +== Boost 1.91 + +* feat/backmp11: Applied further optimizations to achieve ~20% compile time & memory usage reduction for the `favor_runtime_speed` policy compared to the Boost 1.90 release +* feat/backmp11: Support for conditional event deferral with the `deferred_events` property + == Boost 1.90 * feat/backmp11: New back-end backmp11, requires C++ 17 diff --git a/include/boost/msm/backmp11/detail/favor_runtime_speed.hpp b/include/boost/msm/backmp11/detail/favor_runtime_speed.hpp index b5e73846..286eacf1 100644 --- a/include/boost/msm/backmp11/detail/favor_runtime_speed.hpp +++ b/include/boost/msm/backmp11/detail/favor_runtime_speed.hpp @@ -86,12 +86,12 @@ struct compile_policy_impl class is_event_deferred_helper { public: - static bool execute(const StateMachine& sm) + static bool execute(const StateMachine& sm, const Event& event) { bool result = false; - auto visitor = [&result](const auto& /*state*/) + auto visitor = [&result, &sm, &event](const auto& state) { - result = true; + result |= state.is_event_deferred(event, sm); }; // Apply a pre-filter with the 'has_deferred_events' predicate, // since this subset needs to be instantiated only once for the SM. @@ -107,10 +107,10 @@ struct compile_policy_impl }; template - static bool is_event_deferred(const StateMachine& sm, const Event&) + static bool is_event_deferred(const StateMachine& sm, const Event& event) { using helper = is_event_deferred_helper; - return helper::execute(sm); + return helper::execute(sm, event); } template diff --git a/include/boost/msm/backmp11/favor_compile_time.hpp b/include/boost/msm/backmp11/favor_compile_time.hpp index 0a04a22e..354c1bd7 100644 --- a/include/boost/msm/backmp11/favor_compile_time.hpp +++ b/include/boost/msm/backmp11/favor_compile_time.hpp @@ -108,36 +108,63 @@ struct compile_policy_impl template using has_deferred_event = has_deferred_events; - template - static const std::unordered_set& get_deferred_event_type_indices() + // Helper class to check whether a state conditionally defers an event. + template + class is_event_deferred_dispatch_table { - static std::unordered_set type_indices = []() + public: + template + static bool dispatch(const StateMachine& sm, const State& state, const any_event& event) + { + const auto& table = is_event_deferred_dispatch_table::instance(mp11::mp_identity{}); + auto it = table.m_entries.find(event.type()); + if (it != table.m_entries.end()) + { + using real_cell = bool(*)(const StateMachine&, const State&, const any_event&); + auto cell = reinterpret_cast(it->second); + return (*cell)(sm, state, event); + } + return false; + } + + private: + template + is_event_deferred_dispatch_table(mp11::mp_identity) { - std::unordered_set indices; using deferred_events = to_mp_list_t; using deferred_event_identities = mp11::mp_transform; mp11::mp_for_each( - [&indices](auto event_identity) + [this](auto event_identity) { using Event = typename decltype(event_identity)::type; - indices.emplace(to_type_index()); - } - ); - return indices; - }(); - return type_indices; - } + m_entries[to_type_index()] = + reinterpret_cast(&convert_and_execute); + }); + } + + template + static bool convert_and_execute(const StateMachine& sm, const State& state, const any_event& event) + { + return state.is_event_deferred(*any_cast(&event), sm); + } + + template + static const is_event_deferred_dispatch_table& instance(mp11::mp_identity state_identity) + { + static const is_event_deferred_dispatch_table table{state_identity}; + return table; + } + + std::unordered_map m_entries; + }; template static bool is_event_deferred(const StateMachine& sm, const any_event& event) { bool result = false; - const std::type_index type_index = event.type(); - auto visitor = [&result, type_index](const auto& state) + auto visitor = [&result, &sm, &event](const auto& state) { - using State = std::decay_t; - const auto& set = get_deferred_event_type_indices(); - result |= (set.find(type_index) != set.end()); + result |= is_event_deferred_dispatch_table::dispatch(sm, state, event); }; sm.template visit_if(visitor); diff --git a/include/boost/msm/front/detail/common_states.hpp b/include/boost/msm/front/detail/common_states.hpp index 35c71b99..8fe65da3 100644 --- a/include/boost/msm/front/detail/common_states.hpp +++ b/include/boost/msm/front/detail/common_states.hpp @@ -70,10 +70,15 @@ struct state_base : public inherit_attributes, USERBASE // empty implementation for the states not wishing to define an entry condition // will not be called polymorphic way - template - void on_entry(Event const& ,FSM&){} - template - void on_exit(Event const&,FSM& ){} + template + void on_entry(Event const&, FSM&) {} + template + void on_exit(Event const&, FSM&) {} + template + bool is_event_deferred(Event const&, FSM&) const + { + return true; + } // default (empty) transition table; typedef ::boost::mpl::vector<> internal_transition_table; typedef ::boost::fusion::vector<> internal_transition_table11; diff --git a/include/boost/msm/front/puml/puml.hpp b/include/boost/msm/front/puml/puml.hpp index 671c19e5..8fe7c91d 100644 --- a/include/boost/msm/front/puml/puml.hpp +++ b/include/boost/msm/front/puml/puml.hpp @@ -101,6 +101,12 @@ namespace detail { } }); } + // functions added for front::state compatibility + template + bool is_event_deferred(Event const&, FSM&) const + { + return true; + } // typedefs added for front::state compatibility typedef ::boost::mpl::vector<> internal_transition_table; typedef ::boost::fusion::vector<> internal_transition_table11; diff --git a/test/Backmp11Deferred.cpp b/test/Backmp11Deferred.cpp index 4687aaec..f77b407a 100644 --- a/test/Backmp11Deferred.cpp +++ b/test/Backmp11Deferred.cpp @@ -26,27 +26,46 @@ using namespace boost::msm::backmp11; namespace mp11 = boost::mp11; // Events -struct Event1 {}; -struct Event2 {}; -struct ManualDeferEvent {}; +struct Event1 +{ + size_t id{}; +}; +struct Event2 +{ + size_t id{}; +}; +struct Event3 +{ + bool to_be_deferred{false}; + size_t id{}; +}; struct FromHandleAllToHandleNone {}; struct FromHandleNoneToHandleAll {}; -struct FromDeferEvent1And2ToDeferEvent1 {}; +struct FromDeferAllToDeferEvent1 {}; struct FromDeferEvent1ToHandleNone {}; // Actions struct Action { template - void operator()(const Event1&, Fsm& fsm, SourceState&, TargetState&) + void operator()(const Event1& event, Fsm& fsm, SourceState&, TargetState&) { fsm.event1_action_calls++; + fsm.event1_processed_ids.push_back(event.id); } template - void operator()(const Event2&, Fsm& fsm, SourceState&, TargetState&) + void operator()(const Event2& event, Fsm& fsm, SourceState&, TargetState&) { fsm.event2_action_calls++; + fsm.event2_processed_ids.push_back(event.id); + } + + template + void operator()(const Event3& event, Fsm& fsm, SourceState&, TargetState&) + { + fsm.event3_action_calls++; + fsm.event3_processed_ids.push_back(event.id); } }; @@ -71,11 +90,23 @@ struct StateMachineBase_ : state_machine_def return result; } + bool check_and_reset_event3_action_calls(size_t expected) + { + const bool result = (event3_action_calls == expected); + event3_action_calls = 0; + return result; + } + size_t event1_action_calls{}; + std::vector event1_processed_ids; size_t event2_action_calls{}; + std::vector event2_processed_ids; + size_t event3_action_calls{}; + std::vector event3_processed_ids; }; -namespace uml_deferred +// Test event deferral with the deferred_events property. +namespace deferred_events_test { struct StateDeferEvent1 : state<> @@ -83,24 +114,33 @@ struct StateDeferEvent1 : state<> using deferred_events = mp11::mp_list; }; -struct StateDeferEvent1And2 : state<> +struct StateDeferAll : state<> { - using deferred_events = mp11::mp_list; + using deferred_events = mp11::mp_list; +}; + +struct StateDeferEvent3Conditionally : state<> +{ + using deferred_events = mp11::mp_list; + + template + bool is_event_deferred(const Event3& event, Fsm&) const + { + return event.to_be_deferred; + } }; struct StateMachine_ : StateMachineBase_ { using initial_state = - mp11::mp_list; + mp11::mp_list; using transition_table = mp11::mp_list< - Row, - Row, - Row, - Row, - Row, - Row, - Row + Row, + Row, + Row, + Row, + Row >; }; @@ -113,62 +153,83 @@ using Fsms = mp11::mp_list< >; -BOOST_AUTO_TEST_CASE_TEMPLATE(uml_deferred, Fsm, Fsms) +BOOST_AUTO_TEST_CASE_TEMPLATE(deferred_events_test, Fsm, Fsms) { Fsm fsm; fsm.start(); - fsm.process_event(Event1{}); + fsm.process_event(Event1{0}); BOOST_REQUIRE(fsm.check_and_reset_event1_action_calls(0)); BOOST_REQUIRE(fsm.check_and_reset_event2_action_calls(0)); BOOST_REQUIRE(fsm.get_deferred_events_queue().size() == 1); - fsm.process_event(Event2{}); + fsm.process_event(Event1{1}); BOOST_REQUIRE(fsm.check_and_reset_event1_action_calls(0)); BOOST_REQUIRE(fsm.check_and_reset_event2_action_calls(0)); BOOST_REQUIRE(fsm.get_deferred_events_queue().size() == 2); + + fsm.process_event(Event2{0}); + BOOST_REQUIRE(fsm.check_and_reset_event1_action_calls(0)); + BOOST_REQUIRE(fsm.check_and_reset_event2_action_calls(0)); + BOOST_REQUIRE(fsm.get_deferred_events_queue().size() == 3); + + // StateDeferEvent3Conditionally would process, + // but StateDeferAll defers the event. + fsm.process_event(Event3{false, 0}); + BOOST_REQUIRE(fsm.check_and_reset_event1_action_calls(0)); + BOOST_REQUIRE(fsm.check_and_reset_event2_action_calls(0)); + BOOST_REQUIRE(fsm.check_and_reset_event3_action_calls(0)); + BOOST_REQUIRE(fsm.get_deferred_events_queue().size() == 4); - fsm.process_event(FromDeferEvent1And2ToDeferEvent1{}); + fsm.process_event(FromDeferAllToDeferEvent1{}); BOOST_REQUIRE(fsm.check_and_reset_event1_action_calls(0)); BOOST_REQUIRE(fsm.check_and_reset_event2_action_calls(1)); - BOOST_REQUIRE(fsm.get_deferred_events_queue().size() == 1); + BOOST_REQUIRE(fsm.check_and_reset_event3_action_calls(1)); + BOOST_REQUIRE(fsm.get_deferred_events_queue().size() == 2); fsm.process_event(FromDeferEvent1ToHandleNone{}); - BOOST_REQUIRE(fsm.check_and_reset_event1_action_calls(1)); + BOOST_REQUIRE(fsm.check_and_reset_event1_action_calls(2)); + BOOST_REQUIRE(fsm.event1_processed_ids.at(0) == 0); + BOOST_REQUIRE(fsm.event1_processed_ids.at(1) == 1); BOOST_REQUIRE(fsm.check_and_reset_event2_action_calls(0)); BOOST_REQUIRE(fsm.get_deferred_events_queue().size() == 0); - fsm.process_event(ManualDeferEvent{}); + // The event gets conditionally deferred. + fsm.process_event(Event3{true, 1}); BOOST_REQUIRE(fsm.check_and_reset_event1_action_calls(0)); BOOST_REQUIRE(fsm.check_and_reset_event2_action_calls(0)); + BOOST_REQUIRE(fsm.check_and_reset_event3_action_calls(0)); BOOST_REQUIRE(fsm.get_deferred_events_queue().size() == 1); - // The new event gets deferred: +1 - // The deferred event gets consumed and deferred again: -1, +1 - fsm.process_event(ManualDeferEvent{}); + // The previous event stays conditionally deferred, + // the new event gets consumed. + fsm.process_event(Event3{false, 2}); BOOST_REQUIRE(fsm.check_and_reset_event1_action_calls(0)); BOOST_REQUIRE(fsm.check_and_reset_event2_action_calls(0)); - BOOST_REQUIRE(fsm.get_deferred_events_queue().size() == 2); + BOOST_REQUIRE(fsm.check_and_reset_event3_action_calls(1)); + BOOST_REQUIRE(fsm.event3_processed_ids.at(0) == 0); + BOOST_REQUIRE(fsm.event3_processed_ids.at(1) == 2); + BOOST_REQUIRE(fsm.get_deferred_events_queue().size() == 1); fsm.stop(); } -} // namespace uml_deferred +} // namespace deferred_events_test // Test case for manual deferral by using transitions with Defer actions. // Not specified in UML and thus no clear semantics how it should behave. // Currently a Defer action consumes the event (it gets removed from the queue) // and then defers it (it gets pushed back to the queue), the Defer action // returns HANDLED_DEFERRED as processing result). -namespace action_deferred +namespace action_deferred_test { struct StateDeferEvent1 : state<> { }; -struct StateDeferEvent1And2 : state<> +struct StateDeferAll : state<> { }; @@ -176,18 +237,16 @@ struct StateMachine_ : StateMachineBase_ { using activate_deferred_events = int; using initial_state = - mp11::mp_list; + mp11::mp_list; using transition_table = mp11::mp_list< - Row, - Row, - Row, - Row, - Row, - Row, - Row, - Row, - Row + Row, + Row, + Row, + Row, + Row, + Row, + Row >; }; @@ -200,29 +259,29 @@ using Fsms = mp11::mp_list< >; -BOOST_AUTO_TEST_CASE_TEMPLATE(action_deferred, Fsm, Fsms) +BOOST_AUTO_TEST_CASE_TEMPLATE(action_deferred_test, Fsm, Fsms) { Fsm fsm; fsm.start(); fsm.process_event(Event1{}); - // Processed by StateHandleAll, deferred by StateDeferEvent1And2. + // Processed by StateHandleAll, deferred by StateDeferAll. // Queue: Event1 BOOST_REQUIRE(fsm.check_and_reset_event1_action_calls(1)); BOOST_REQUIRE(fsm.get_deferred_events_queue().size() == 1); fsm.process_event(Event2{}); - // Processed by StateHandleAll, deferred by StateDeferEvent1And2. - // StateHandleAll also processes Event1 again. + // Processed by StateHandleAll, deferred by StateDeferAll. + // StateHandleAll processes Event1 a 2nd time. // Queue: Event2, Event1 BOOST_REQUIRE(fsm.check_and_reset_event1_action_calls(1)); BOOST_REQUIRE(fsm.check_and_reset_event2_action_calls(1)); BOOST_REQUIRE(fsm.get_deferred_events_queue().size() == 2); - fsm.process_event(FromDeferEvent1And2ToDeferEvent1{}); + fsm.process_event(FromDeferAllToDeferEvent1{}); // Event2 is no more deferred. - // StateHandleAll processes Event2 & Event1, + // StateHandleAll processes Event2 a 2nd time & Event1 a 3rd time, // StateDeferEvent1 defers Event1. // Queue: Event1 BOOST_REQUIRE(fsm.check_and_reset_event1_action_calls(1)); @@ -231,11 +290,11 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(action_deferred, Fsm, Fsms) fsm.process_event(FromDeferEvent1ToHandleNone{}); // Event1 is no more deferred. - // StateHandleAll processes Event1. + // StateHandleAll processes Event1 a 4th time. BOOST_REQUIRE(fsm.check_and_reset_event1_action_calls(1)); BOOST_REQUIRE(fsm.get_deferred_events_queue().size() == 0); fsm.stop(); } -} // namespace action_deferred +} // namespace action_deferred_test