feat(backmp11): simplified functor signatures

This commit is contained in:
Christian Granzin
2026-02-19 13:27:44 -05:00
committed by Christian Granzin
parent d0cf98bd39
commit 428cbad053
9 changed files with 502 additions and 10 deletions
@@ -15,8 +15,8 @@ It offers a significant improvement in runtime and memory usage, as can be seen
| back | 14 | 815 | 68 | 2.8
| back_favor_compile_time | 17 | 775 | 226 | 3.5
| back11 | 37 | 2682 | 84 | 2.8
| backmp11 | 3 | 209 | 29 | 0.7
| backmp11_favor_compile_time | 3 | 193 | 43 | 6.0
| backmp11 | 3 | 211 | 29 | 0.7
| backmp11_favor_compile_time | 3 | 195 | 43 | 6.0
| sml | 5 | 234 | 57 | 0.3
|=======================================================================================================
@@ -28,8 +28,8 @@ 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 | 49 | 2165 | 230 | 13.2
| back_favor_compile_time | 55 | 1704 | 911 | > 300
| backmp11 | 8 | 351 | 85 | 3.3
| backmp11_favor_compile_time | 5 | 256 | 100 | 20.4
| backmp11 | 8 | 354 | 85 | 3.4
| backmp11_favor_compile_time | 5 | 262 | 100 | 20.4
| backmp11_favor_compile_time_multi_cu | 5 | ~863 | 100 | 20.8
| sml | 18 | 543 | 422 | 5.4
|================================================================================================================
@@ -170,6 +170,10 @@ If the type of the state appears multiple times in a hierarchical state machine,
// - if `start()` is called for a running state machine, the call is ignored
// - if `stop()` is called on a stopped (not running) state machine, the call is ignored
=== Support for simplified functor signatures
Further described in xref:./functor-front-end.adoc#simplified_functor_signatures[the functor front-end documentation].
=== Simplified state machine signature
@@ -53,7 +53,6 @@ Row < Paused , stop , Stopped , stop_playback , none
Row < Paused , open_close , Open , stop_and_open , none >
// +---------+------------+-----------+---------------------------+----------------------------+
> {};
----
Transitions are now of type "Row" with exactly 5 template arguments:
@@ -66,12 +65,12 @@ detected event, the state machine, source and target state:
----
struct store_cd_info
{
template <class Fsm,class Evt,class SourceState,class TargetState>
void operator()(Evt const&, Fsm& fsm, SourceState&,TargetState& )
template <class Event, class Fsm, class SourceState, class TargetState>
void operator()(Event const&, Fsm& fsm, SourceState&, TargetState&)
{
cout << "player::store_cd_info" << endl;
fsm.process_event(play());
}
}
};
----
@@ -119,6 +118,88 @@ It even starts looking like functional programming. MSM ships with
functors for operators, state machine usage, STL algorithms or container
methods.
== Using lambdas as functors ({cpp}20)
If {cpp}20 is available you can shorten the functor syntax by using lambdas as functors.
The `store_cd_info` functor can be rewritten with a lambda as follows:
[source,cpp]
----
using store_cd_info = msm::front::Lambda<
[](auto const& /*event*/, auto& fsm, auto& /*source*/, auto& /*target*/)
{
cout << "player::store_cd_info" << endl;
fsm.process_event(play());
}>;
----
[[simplified_functor_signatures]]
== Simplified functor signatures (`backmp11` only)
The `backmp11` back-end allows you to use shorter functor signatures, reducing the boilerplate of action and guard functor definitions. It automatically detects how many parameters your functors have and invokes them with the appropriate arguments.
The following three signatures are supported:
**Full signature**
[source,cpp]
----
struct store_cd_info
{
template <class Event, class Fsm, class SourceState, class TargetState>
void operator()(Event const&, Fsm& fsm, SourceState&, TargetState&)
{
cout << "player::store_cd_info" << endl;
fsm.process_event(play());
}
};
----
**Event and FSM only**
[source,cpp]
----
struct store_cd_info
{
template <class Event, class Fsm>
void operator()(Event const&, Fsm& fsm)
{
cout << "player::store_cd_info" << endl;
fsm.process_event(play());
}
};
----
**FSM only**
[source,cpp]
----
struct store_cd_info
{
template <class Fsm>
void operator()(Fsm& fsm)
{
cout << "player::store_cd_info" << endl;
fsm.process_event(play());
}
};
----
Simplified signatures are also supported with lambdas, providing a way to define
functors with minimal boilerplate:
[source,cpp]
----
using store_cd_info = msm::front::Lambda<
[](auto& fsm)
{
cout << "player::store_cd_info" << endl;
fsm.process_event(play());
}>;
----
[[defining-states-with-entryexit-actions]]
== Defining states with entry/exit actions
@@ -12,6 +12,7 @@
* feat(backmp11): Small Object Optimization for events in the event pool (https://github.com/boostorg/msm/issues/172[#172])
* feat(backmp11): Improve support for the `deferred_events` property in hierarchical state machines (https://github.com/boostorg/msm/issues/173[#173])
* feat(backmp11): Improve runtime performance with a `flat_fold` dispatch strategy (https://github.com/boostorg/msm/issues/180[#180])
* feat(backmp11): Simplified functor signatures (https://github.com/boostorg/msm/issues/175[#175])
== Boost 1.90
@@ -18,9 +18,111 @@
#include <boost/msm/backmp11/detail/metafunctions.hpp>
#include "boost/msm/backmp11/state_machine_config.hpp"
namespace boost::msm::front
{
struct Defer;
};
namespace boost::msm::backmp11::detail
{
// Chain of priority tags for SFINAE handling:
// priority_tag_0
// ↓ (SFINAE fails?)
// priority_tag_1 (base of priority_tag_0)
// ↓ (SFINAE fails?)
// priority_tag_2 (base of priority_tag_1)
struct priority_tag_2 {};
struct priority_tag_1 : priority_tag_2 {};
struct priority_tag_0 : priority_tag_1 {};
template <typename Functor, typename Event, typename Fsm, typename Source,
typename Target>
auto invoke_functor(priority_tag_0, Functor&&, const Event& event, Fsm& fsm,
Source& source, Target& target)
-> decltype(Functor{}(event, fsm, source, target))
{
return Functor{}(event, fsm, source, target);
}
template <typename Functor, typename Event, typename Fsm, typename Source,
typename Target>
auto invoke_functor(priority_tag_1, Functor&&, const Event& event, Fsm& fsm, Source&,
Target&) -> decltype(Functor{}(event, fsm))
{
return Functor{}(event, fsm);
}
template <typename Functor, typename Event, typename Fsm, typename Source,
typename Target>
auto invoke_functor(priority_tag_2, Functor&&, const Event&, Fsm& fsm, Source&,
Target&) -> decltype(Functor{}(fsm))
{
return Functor{}(fsm);
}
template <typename Row>
using get_Guard = typename Row::Guard;
template <typename Row>
struct has_Guard : mp11::mp_valid<get_Guard, Row> {};
template <typename Functor>
struct invoke_guard_functor
{
template <typename Event, typename Fsm, typename Source, typename Target>
static bool execute(const Event& event, Fsm& fsm, Source& source,
Target& target)
{
return invoke_functor<Functor>(priority_tag_0{}, Functor{}, event, fsm,
source, target);
}
};
template <>
struct invoke_guard_functor<front::none>
{
template <typename Event, typename Fsm, typename Source, typename Target>
static bool execute(const Event&, Fsm&, Source&, Target&)
{
return true;
}
};
template <typename Row>
using get_Action = typename Row::Action;
template <typename Row>
struct has_Action : mp11::mp_valid<get_Action, Row> {};
template <typename Functor>
struct invoke_action_functor
{
template <typename Event, typename Fsm, typename Source, typename Target>
static process_result execute(const Event& event, Fsm& fsm, Source& source,
Target& target)
{
invoke_functor<Functor>(priority_tag_0{}, Functor{}, event, fsm, source,
target);
return process_result::HANDLED_TRUE;
}
};
template <>
struct invoke_action_functor<front::none>
{
template <typename Event, typename Fsm, typename Source, typename Target>
static process_result execute(const Event&, Fsm&, Source&, Target&)
{
return process_result::HANDLED_TRUE;
}
};
template <>
struct invoke_action_functor<front::Defer>
{
template <typename Event, typename Fsm, typename Source, typename Target>
static process_result execute(const Event& event, Fsm& fsm, Source&,
Target&)
{
fsm.defer_event(event);
return process_result::HANDLED_DEFERRED;
}
};
template <typename StateMachine>
struct transition_table_impl
{
@@ -39,7 +141,12 @@ struct transition_table_impl
static bool call_guard_or_true(StateMachine& sm, const Event& event,
Source& source, Target& target)
{
if constexpr (HasGuard)
if constexpr (has_Guard<Row>::value)
{
return invoke_guard_functor<typename Row::Guard>::execute(
event, sm.get_fsm_argument(), source, target);
}
else if constexpr (HasGuard)
{
return Row::guard_call(
sm.get_fsm_argument(), event, source, target, sm.m_states);
@@ -49,13 +156,19 @@ struct transition_table_impl
return true;
}
}
template <typename Row, bool HasAction, typename Event, typename Source,
typename Target>
static process_result call_action_or_true(StateMachine& sm,
const Event& event,
Source& source, Target& target)
{
if constexpr (HasAction)
if constexpr (has_Action<Row>::value)
{
return invoke_action_functor<typename Row::Action>::execute(
event, sm.get_fsm_argument(), source, target);
}
else if constexpr (HasAction)
{
return Row::action_call(
sm.get_fsm_argument(), event, source, target, sm.m_states);
+7
View File
@@ -361,5 +361,12 @@ namespace boost { namespace msm { namespace front
fsm.defer_event(evt);
}
};
#if __cplusplus >= 202002L
// Wrapper to make a functor from a lambda.
template <auto T>
struct Lambda : decltype(T) {};
#endif
}}}
#endif //BOOST_MSM_FRONT_FUNCTOR_ROW_H
+282
View File
@@ -0,0 +1,282 @@
// Copyright 2026 Christian Granzin
// Copyright 2024 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_completion
#endif
#include <boost/test/unit_test.hpp>
// back-end
#include "Backmp11.hpp"
// #include "BackCommon.hpp"
//front-end
#include "FrontCommon.hpp"
#include "Utils.hpp"
using namespace boost::msm::front;
using namespace boost::msm::backmp11;
namespace mp11 = boost::mp11;
namespace
{
// Events
struct TriggerOnlyFsmActionLambda {};
struct TriggerDefaultAction {};
struct TriggerOnlyEventAndFsmAction {};
struct TriggerOnlyFsmAction {};
struct TriggerActionWithMultipleOverloads {};
struct TriggerSpecificAction {};
// States
struct State1 : test::StateBase
{
};
struct State2 : test::StateBase
{
};
struct Machine_;
// Actions
struct DefaultAction
{
template <typename Event, typename Fsm, typename Source, typename Target>
void operator()(const Event&, Fsm&, Source&, Target&)
{
static_assert(std::is_same_v<Event, TriggerDefaultAction>);
static_assert(std::is_base_of_v<Machine_, Fsm>);
calls += 1;
}
[[maybe_unused]] static inline size_t calls{};
};
struct OnlyEventAndFsmAction
{
template <typename Event, typename Fsm>
void operator()(const Event&, Fsm&)
{
static_assert(std::is_same_v<Event, TriggerOnlyEventAndFsmAction>);
static_assert(std::is_base_of_v<Machine_, Fsm>);
calls += 1;
}
[[maybe_unused]] static inline size_t calls{};
};
struct OnlyFsmAction
{
template <typename Fsm>
void operator()(Fsm&)
{
static_assert(std::is_base_of_v<Machine_, Fsm>);
calls += 1;
}
[[maybe_unused]] static inline size_t calls{};
};
struct ActionWithMultipleOverloads : DefaultAction
{
template <typename Event, typename Fsm, typename Source, typename Target>
void operator()(const Event&, Fsm&, Source&, Target&)
{
static_assert(std::is_same_v<Event, TriggerActionWithMultipleOverloads>);
static_assert(std::is_base_of_v<Machine_, Fsm>);
default_calls += 1;
}
[[maybe_unused]] static inline size_t default_calls{};
// Overload constraint because caused by priority tags in this edge case:
// A specific overload must use the highest used arity of the generic overloads.
template <typename Fsm, typename Source, typename Target>
void operator()(const TriggerSpecificAction&, Fsm&, Source&, Target&)
{
static_assert(std::is_base_of_v<Machine_, Fsm>);
specific_calls += 1;
}
[[maybe_unused]] static inline size_t specific_calls{};
template <typename Event, typename Fsm>
void operator()(const Event&, Fsm&)
{
static_assert(std::is_same_v<Event, TriggerActionWithMultipleOverloads>);
static_assert(std::is_base_of_v<Machine_, Fsm>);
only_event_and_fsm_calls += 1;
}
[[maybe_unused]] static inline size_t only_event_and_fsm_calls{};
template <typename Fsm>
void operator()(Fsm&)
{
static_assert(std::is_base_of_v<Machine_, Fsm>);
only_fsm_calls += 1;
}
[[maybe_unused]] static inline size_t only_fsm_calls{};
};
// Lambda support for shorter syntax (requires C++20).
#if __cplusplus >= 202002L
using OnlyFsmActionLambda = Lambda<[](auto& fsm) {
using Fsm = std::decay_t<decltype(fsm)>;
static_assert(std::is_base_of_v<Machine_, Fsm>);
}>;
#endif
// Guards
struct DefaultGuard
{
template <typename Event, typename Fsm, typename Source, typename Target>
bool operator()(const Event&, Fsm&, Source&, Target&)
{
static_assert(std::is_same_v<Event, TriggerDefaultAction>);
static_assert(std::is_base_of_v<Machine_, Fsm>);
calls += 1;
return true;
}
[[maybe_unused]] static inline size_t calls{};
};
struct OnlyEventAndFsmGuard
{
template <typename Event, typename Fsm>
bool operator()(const Event&, Fsm&)
{
static_assert(std::is_same_v<Event, TriggerOnlyEventAndFsmAction>);
static_assert(std::is_base_of_v<Machine_, Fsm>);
calls += 1;
return true;
}
[[maybe_unused]] static inline size_t calls{};
};
struct OnlyFsmGuard
{
template <typename Fsm>
bool operator()(Fsm&)
{
static_assert(std::is_base_of_v<Machine_, Fsm>);
calls += 1;
return true;
}
[[maybe_unused]] static inline size_t calls{};
};
struct GuardWithMultipleOverloads
{
template <typename Event, typename Fsm, typename Source, typename Target>
bool operator()(const Event&, Fsm&, Source&, Target&)
{
static_assert(std::is_same_v<Event, TriggerActionWithMultipleOverloads>);
static_assert(std::is_base_of_v<Machine_, Fsm>);
default_calls += 1;
return true;
}
[[maybe_unused]] static inline size_t default_calls{};
// Overload constraint because caused by priority tags in this edge case:
// A specific overload must use the highest used arity of the generic overloads.
template <typename Fsm, typename Source, typename Target>
bool operator()(const TriggerSpecificAction&, Fsm&, Source&, Target&)
{
static_assert(std::is_base_of_v<Machine_, Fsm>);
specific_calls += 1;
return true;
}
[[maybe_unused]] static inline size_t specific_calls{};
template <typename Event, typename Fsm>
bool operator()(const Event&, Fsm&)
{
static_assert(std::is_same_v<Event, TriggerActionWithMultipleOverloads>);
static_assert(std::is_base_of_v<Machine_, Fsm>);
only_event_and_fsm_calls += 1;
return true;
}
[[maybe_unused]] static inline size_t only_event_and_fsm_calls{};
template <typename Fsm>
bool operator()(Fsm&)
{
static_assert(std::is_base_of_v<Machine_, Fsm>);
only_fsm_calls += 1;
return true;
}
[[maybe_unused]] static inline size_t only_fsm_calls{};
};
// Lambda support for shorter syntax (requires C++20).
#if __cplusplus >= 202002L
using OnlyFsmGuardLambda = Lambda<[](auto& fsm) {
using Fsm = std::decay_t<decltype(fsm)>;
static_assert(std::is_base_of_v<Machine_, Fsm>);
return true;
}>;
#endif
struct Machine_ : test::StateMachineBase_<Machine_>
{
using initial_state = State1;
using transition_table = mp11::mp_list<
// Start Event Next Action Guard
#if __cplusplus >= 202002L
Row < State1, TriggerOnlyFsmActionLambda , none, OnlyFsmActionLambda , OnlyFsmGuardLambda >,
#endif
Row < State1, TriggerDefaultAction , none, DefaultAction , DefaultGuard >,
Row < State1, TriggerOnlyEventAndFsmAction , none, OnlyEventAndFsmAction , OnlyEventAndFsmGuard >,
Row < State1, TriggerOnlyFsmAction , none, OnlyFsmAction , OnlyFsmGuard >,
Row < State1, TriggerActionWithMultipleOverloads, none, ActionWithMultipleOverloads, GuardWithMultipleOverloads >,
Row < State1, TriggerSpecificAction , none, ActionWithMultipleOverloads, GuardWithMultipleOverloads >
>;
};
// Pick a back-end
using TestMachines = mp11::mp_list<
#ifndef BOOST_MSM_TEST_SKIP_BACKMP11
state_machine<Machine_>,
state_machine<Machine_, favor_compile_time_config>
#endif // BOOST_MSM_TEST_SKIP_BACKMP11
>;
BOOST_AUTO_TEST_CASE_TEMPLATE(test, TestMachine, TestMachines)
{
TestMachine state_machine;
state_machine.start();
state_machine.process_event(TriggerDefaultAction());
ASSERT_ONE_AND_RESET(DefaultAction::calls);
ASSERT_ONE_AND_RESET(DefaultGuard::calls);
state_machine.process_event(TriggerOnlyEventAndFsmAction());
ASSERT_ONE_AND_RESET(OnlyEventAndFsmAction::calls);
ASSERT_ONE_AND_RESET(OnlyEventAndFsmGuard::calls);
state_machine.process_event(TriggerOnlyFsmAction());
ASSERT_ONE_AND_RESET(OnlyFsmAction::calls);
ASSERT_ONE_AND_RESET(OnlyFsmGuard::calls);
state_machine.process_event(TriggerActionWithMultipleOverloads());
ASSERT_ONE_AND_RESET(ActionWithMultipleOverloads::default_calls);
ASSERT_ONE_AND_RESET(GuardWithMultipleOverloads::default_calls);
#if __cplusplus >= 202002L
state_machine.process_event(TriggerOnlyFsmActionLambda());
#endif
}
} // namespace
+1
View File
@@ -78,6 +78,7 @@ add_executable(boost_msm_cxx17_tests
Backmp11Context.cpp
Backmp11Deferred.cpp
Backmp11EntryExit.cpp
Backmp11FunctorApi.cpp
Backmp11ManyDeferTransitions.cpp
Backmp11RootSm.cpp
Backmp11Transitions.cpp
+1
View File
@@ -81,6 +81,7 @@ test-suite msm-unit-tests-cxxstd17
[ run Backmp11Constructor.cpp ]
[ run Backmp11Context.cpp ]
[ run Backmp11Deferred.cpp ]
[ run Backmp11FunctorApi.cpp ]
[ run Backmp11EntryExit.cpp ]
[ run Backmp11ManyDeferTransitions.cpp ]
[ run Backmp11RootSm.cpp ]
+2
View File
@@ -9,6 +9,8 @@
// http://www.boost.org/LICENSE_1_0.txt)
// back-end
// Backmp11 does not support custom Defer actions.
#define BOOST_MSM_TEST_SKIP_BACKMP11
#include "BackCommon.hpp"
//front-end
#include <boost/msm/front/state_machine_def.hpp>