Backmp11 back-end (C++17, experimental)
Backmp11 is a new back-end that is mostly backwards-compatible with back.
It is currently in experimental stage, thus some details about the compatibility might change (feedback welcome!).
It is named after the metaprogramming library Boost Mp11, the main contributor to the optimizations.
Usages of MPL are replaced with Mp11 to get rid of the costly C++03 emulation of variadic templates.
It offers a significant improvement in runtime and memory usage, as can be seen in these benchmarks:
Large state machine
| Compile time / sec | Compile RAM / MB | Binary size / kB | Runtime / sec | |
|---|---|---|---|---|
back |
10 |
844 |
73 |
0.7 |
back_favor_compile_time |
12 |
821 |
241 |
1.0 |
back11 |
26 |
2675 |
92 |
0.7 |
backmp11 |
2 |
237 |
21 |
0.4 |
backmp11_favor_compile_time |
2 |
225 |
45 |
2.2 |
sml |
3 |
242 |
57 |
0.1 |
Large hierarchical state machine
| 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 |
362 |
52 |
1.8 |
backmp11_favor_compile_time |
3 |
263 |
98 |
7.6 |
backmp11_favor_compile_time_multi_cu |
3 |
~934 |
99 |
8.6 |
sml |
12 |
567 |
436 |
2.5 |
The full code with the benchmarks and more information about them is available in this repository. The tables in the repository are frequently updated with results from the latest development branches of the benchmarked libraries.
Creation
The state_machine signature in backmp11 looks as follows (pseudo-code):
template <
typename FrontEnd,
typename Config = default_state_machine_config,
typename Derived = state_machine
>
class state_machine;
Only the FrontEnd parameter is mandatory, using my_fsm = msm::backmp11::state_machine<my_front_end>; is the minimal declaration to create a state machine.
Configuration
A state machine’s configuration can be adjusted by passing a custom Config parameter. The default configuration is:
// Default config:
struct default_state_machine_config
{
// A common context that is shared by all SMs
// in hierarchical state machines.
using context = no_context;
// Tune characteristics related to compile time, runtime performance,
// code size, and available features.
using compile_policy = favor_runtime_speed;
// Which container to use for the event pool.
template <typename T>
using event_pool_container = std::deque<T>;
// Type of the Fsm parameter passed in actions and guards.
using fsm_parameter = local_transition_owner;
// Identifier for the upper-most SM
// in hierarchical state machines.
using root_sm = no_root_sm;
};
using state_machine_config = default_state_machine_config;
Inherit from state_machine_config and override the using directives to create a custom configuration.
// Custom config:
struct CustomStateMachineConfig : state_machine_config
{
using context = Context;
using root_sm = RootSm;
using fsm_parameter = root_sm;
};
The state machine configuration is designed to be shared in hierarchical state machines.
Context
The setting context sets up a context member in the state machine for dependency injection.
If using context = CustomContext; is defined in the config, a reference to it has to be passed to the state machine constructor as first argument.
The following API becomes available to access it in the state machine:
// Context access:
Context& state_machine::get_context();
const Context& state_machine::get_context() const;
Root state machine
The setting root_sm defines the type of the root state machine of hierarchical state machines. The root sm depicts the uppermost state machine.
If using root_sm = RootSm; is defined in the config, the following API becomes available to access it from any submachine:
// Root state machine access:
RootSm& state_machine::get_root_sm();
const RootSm& state_machine::get_root_sm() const;
It is highly recommended to always configure the root_sm in hierarchical state machines, even if access to it is not required.
This reduces the compilation time, because it enables the back-end to instantiate the full set of construction-related methods
only for the root and it can omit them for submachines.
Fsm parameter of actions und guards
The setting fsm_parameter defines the instance of the Fsm& fsm parameter that is passed to actions and guards in hierarchical state machines.
By default it is set to local_transition_owner, which behaves identically to back:
-
Actions and guards for transitions in the same transition table receive the SM instance that owns the transition (the one processing the event)
-
Entry and exit actions receive the "local" transition owner from the perspective of the state being entered/exited (the immediate parent SM)
You can alternatively set it to using fsm_parameter = root_sm, in which case the configured root_sm is passed as Fsm parameter.
|
In UML, the "transition owner" is the region or state machine that contains the transition.
The term "local transition owner" extends this UML terminology, because the Example: Consider a hierarchical state machine with nested state machines:
When a transition defined in SM1 causes SM2 and SM3 to exit:
|
Event pool
The state machine utilizes an event pool for events that are not processed immediately. The default event pool container is a std::deque<T>. You can use a custom event pool container. It has to support push at both ends as well as removal at begin without iterator invalidation, in detail these API calls:
-
push_back(const T&) -
push_front(const T&) -
begin() -
end() -
erase(iterator) -
clear()
You can deactivate the event pool with using event_pool_container = no_event_pool_container<T>;. A state machine requires the event pool to handle completion events, deferred events, and enqueued events.
Compile policy
favor_runtime_speed
The default compile policy prioritizes runtime speed, it evaluates all transitions and generates the dispatch table at compile time.
The dispatch strategy can be tuned by inheriting from favor_runtime_speed and adapting the using dispatch_strategy directive:
struct favor_runtime_speed
{
// Dispatch strategy for processing events.
// Supported strategies:
// - flat_fold (default)
// - function_pointer_array
using dispatch_strategy = dispatch_strategy::flat_fold;
};
It currently supports two dispatch strategies:
| Strategy | Description |
|---|---|
|
Generates a flat fold of inline comparison branches. |
|
Generates an array of function pointers. |
favor_compile_time
This policy improves compile time at the cost of runtime speed. It evaluates transitions lazily and generates the dispatch table at runtime. Like its counterpart in back, it does not support Kleene events.
Events are wrapped into a std::any when they enter event processing to reduce the number of necessary template instances required to generate the state machine.
A hash map contains the dispatch table, with the type index of each event as the key and an array of function pointers to the matching transitions as the value.
With this policy you can compile a state machine across multiple translation units (TUs) with the help of a preprocessor macro. Since the back-end should compile very fast for most state machines, this is an opt-in feature:
-
define
BOOST_MSM_BACKMP11_MANUAL_GENERATIONbefore includingmsm/backmp11/favor_compile_time.hpp -
then generate your state machine back-end(s) with the macro
BOOST_MSM_BACKMP11_GENERATE_STATE_MACHINE(<sm_type>)
You can find an example for this in the visitor test.
Extension
You can extend a state machine by inheriting from the state_machine template instead of a using directive.
If the extended class is required to be available in Fsm parameter, pass it as third parameter to the state_machine template.
History
In backmp11 the history policy is defined in the front-end instead of the back-end.
Defining it there ensures that one state machine config can be shared between multiple back-ends of hierarchical state machines.
// No history (default).
struct no_history {};
// Shallow history.
// For deep history use this policy for all the contained state machines.
template <typename... Events>
struct shallow_history {};
// Shallow history for all events (not UML conform).
// For deep history use this policy for all the contained state machines.
struct always_shallow_history {};
...
// User-defined state machine.
struct Playing_ : public msm::front::state_machine_def<Playing_>
{
using history = msm::front::shallow_history<end_pause>;
...
};
Start and stop
A newly constructed state machine is inactive and does not process events as long as it is not yet started. The void start() method activates the state machine by first calling the state machine front-end’s entry action and then the initial state’s. By default the event passed to the entry actions is an empty struct backmp11::starting. You can pass a custom event with the overload void start(const auto& initial_event).
The void stop() method deactivates the state machine by first calling the active state’s exit action and then the state machine front-end’s. The default event is an empty struct backmp11::stopping, and a similar overload void stop(const auto& final_event) exists to customize it.
Calling start() on an active or stop() on an inactive state machine has no effect.
Handling events
Use process_result process_event(const Event&) to start processing an event while the state machine is idle.
You can enqueue an event for consecutive processing in an action with void enqueue_event(const Event&). The event will be processed immediately after the current event is done processing.
The back-end supports event deferral with the front-end’s deferred_events state property. Deferred events are evaluated in the same order they have been deferred, ensuring FIFO processing semantics. They are stored in the event pool of the state machine that was requested to process the event. In hierarchical state machines this is usually the root state machine, in which case all submachines are able to receive the event upon dispatch. Event deferral in orthogonal regions behaves as described in the UML standard: As long as one active region decides to defer an event, it remains deferred for all regions.
Conditional deferral is a backmp11-exclusive extension of the deferred_events property in states. Deferral can be made conditional by defining a bool is_event_deferred(…) method in the state:
struct MyState : boost::msm::front::state<>
{
using deferred_events = mp11::mp_list<MyEvent>;
template <typename Fsm>
bool is_event_deferred(const MyEvent& event, Fsm& fsm) const
{
// Return true or false to decide
// whether the event shall be deferred.
...
}
};
You can also defer events in transitions by using the front::Defer action. While this mechanism offers additional flexibility for event deferral, it has a couple of limitations:
-
It uses the event pool of the state machine passed by the
Fsmparameter - if this is not the root machine in hierarchical state machines, state machines further up the hierarchy cannot receive the event. -
Action-deferred events get dispatched for evaluation and then put back into the event pool. This requires additional runtime and prevents deferred events from being processed in FIFO order.
-
In orthogonal regions each region evaluates the event independently, but the regions share the event pool of the containing state machine. This leads to the same event being dispatched multiple times.
Check for active states
Use the method bool is_state_active() to query for active states:
template <typename State>
bool state_machine::is_state_active() const;
If the type of the state appears multiple times in a hierarchical state machine, the method returns true if any of the states are active.
Visit states
You can visit all currently active states with a visitor API. In hierarchical state machines submachines are visited recursively.
template <typename Visitor>
void state_machine::visit(Visitor&& visitor);
The visitor functor must support to be called with all existing state types of the state machine.
template <typename State>
void operator()(State& state);
A state machine can be visited in multiple modes:
-
only the active states or all states
-
non-recursive or recursive
The visit function can be customized with a visit_mode template parameter.
// API:
enum class visit_mode
{
// State selection (mutually exclusive).
active_states = 0b001,
all_states = 0b010,
// Traversal mode (not set = non-recursive).
recursive = 0b100,
// All valid combinations.
active_non_recursive = active_states,
active_recursive = active_states | recursive,
all_non_recursive = all_states,
all_recursive = all_states | recursive
};
template <visit_mode Mode, typename Visitor>
void state_machine::visit(Visitor&& visitor);
// Use the pre-defined constants...
state_machine.visit
<visit_mode::all_recursive>
([](auto &state) {/*...*/});
// ... or assemble a mode
state_machine.visit
<visit_mode::all_states | visit_mode::recursive>
([](auto &state) {/*...*/});
Reflection and serialization
The state_machine provides access to all its members recursively with a reflect free function and a visitor pattern:
// namespace boost::msm::backmp11
// Reflect on a state_machine's members with a visitor.
// The visitor has to implement the methods:
// - visit_front_end(auto&& front_end)
// - visit_front_end(auto&& front_end, auto&& reflect)
// - visit_member(const char* key, auto&& member)
// - visit_state(size_t state_id, auto&& state)
// - visit_state(size_t state_id, auto&& state, auto&& reflect)
template <typename FrontEnd, typename Config, typename Derived,
typename Visitor>
void reflect(detail::state_machine_base<FrontEnd, Config, Derived>& sm,
Visitor&& visitor);
|
Do not rely on assumptions about call orders and argument details of the visit methods. The reflection API exposes internal members of the state machine, which are subject to changes. |
The visit methods of the front-end and states contain two overloads. The first one gets called in case the object to be visited does not have reflection, the second one provides a reflect functor argument that triggers the reflection deeper into the object. You can set up reflection for a front-end or state by implementing a reflect member function or alternatively a free function (with MSVC you have to use a member function due to ADL limitations).
struct MyState : boost::msm::front::state<>
{
// Reflect with a member function.
template <typename Visitor>
void reflect(Visitor&& visitor)
{
visitor.visit_member("my_member", my_member);
}
template <typename Visitor>
void reflect(Visitor&& visitor) const
{
visitor.visit_member("my_member", my_member);
}
uint32_t my_member{};
};
// Or reflect with a free function.
template <typename Visitor>
void reflect(MyState& my_state, Visitor&& visitor)
{
visitor.visit_member("my_member", my_state.my_member);
}
template <typename Visitor>
void reflect(const MyState& my_state, Visitor&& visitor)
{
visitor.visit_member("my_member", my_state.my_member);
}
You can use the reflection API for introspection use cases, the most prominent one being serialization of a state machine. backmp11 supports 3 serialization libraries out-of-the-box:
-
Boost.Serialization
-
Boost.JSON
-
nlohmann/json
For each serialization library you can find a corresponding header with serializer code under boost/msm/backmp11/serialization. The serializer expects all objects with non-static members to be serializable, which can be achieved by implementing either reflection or library-specific serialization methods. It is recommended to implement `backmp11’s reflection API, because this mechanism is generic and supports all serialization libraries.
For serialization with Boost.JSON and nlohmann/json you only need to include the corresponding header, for Boost.Serialization you additionally need to provide a serialize method for the (root) state machine. The backmp11 serialization test demonstrates how to use the serialization libraries.
Comparison to back
The backmp11 back-end should be mostly compatible with existing code utilizing the back back-end:
-
for the state machine use
boost::msm::backmp11::state_machinein place ofboost::msm::back::state_machine -
for configuring the compile policy and more use a
boost::msm::backmp11::state_machine_configstruct -
adapt public API calls that have been changed in
backmp11or write an adapter by extending thestate_machineclass
The next paragraphs offer further details about the differences between backmp11 and back.
Public API of state_machine
The following adapter pseudo-code showcases the differences to the state_machine API of back.
class state_machine_adapter
{
using Flag_AND = backmp11::flag_and;
template <typename Event>
back::HandledEnum process_event(const Event& event)
{
if (this->get_machine_state() == detail::machine_state::processing)
{
this->enqueue_event(event);
return back::HANDLED_DEFERRED;
}
else
{
return Base::process_event(event);
}
}
// The new API returns a const std::array<...>&.
const uint16_t* current_state() const
{
return &this->get_active_state_ids()[0];
}
auto& get_message_queue()
{
return this->get_event_pool().events;
}
size_t get_message_queue_size() const
{
return this->get_event_pool().events.size();
}
void execute_queued_events()
{
this->process_event_pool();
}
void execute_single_queued_event()
{
this->process_event_pool(1);
}
auto& get_deferred_queue()
{
return this->get_event_pool().events;
}
void clear_deferred_queue()
{
this->get_event_pool().events.clear();
}
template <class Archive>
void serialize(Archive& ar,
const unsigned int /*version*/)
{
backmp11::reflect(*this, serializer<Archive>{ar});
}
// No adapter.
// Superseded by the visitor API.
// void visit_current_states(...) {...}
// No adapter.
// States can be set with `get_state<...>() = ...` or the visitor API.
// void set_states(...) {...}
// No adapter.
// Could be implemented with the visitor API.
// auto get_state_by_id(int id) {...}
};
A working code example of such an adapter is available in the tests. It can be copied and adapted if needed, though this class is internal to the tests and not planned to be supported officially.
Kleene events
To reduce the amount of necessary header inclusions, backmp11 uses std::any for defining Kleene events instead of boost::any.
You can opt in to support boost::any by including boost/msm/event_traits.h.
Removed features
Initialization of states in the constructor and the set_states(…) method
There were some caveats with one constructor that was used for different use cases: On the one hand some arguments were immediately forwarded to the front-end’s constructor, on the other hand the stream operator was used to identify other arguments in the constructor as states, to copy them into the state machine. Besides the syntax of the later being rather unusual, when doing both at once the syntax becomes too difficult to understand; even more so if states within hierarchical submachines are initialized in this fashion.
In order to keep the API of the constructor simpler and less ambiguous, it only supports forwarding arguments to the front-end.
Also the set_states(…) API is removed. If setting a state is required, this can still be done (in a more verbose, but also more direct & explicit way) by getting a reference to the desired state via get_state<State>() and then assigning a new state object to it.
The get_state_by_id(…) method
If you really need to get a state by id, you can use the visitor API to implement the function on your own.
The state_machine has a method to support getting the id of a state in the visitor:
template <typename State>
static constexpr uint16_t state_machine::get_state_id(const State&);
The pointer overload of get_state<…>()
Similarly to the STL’s std::get<…>(…) method for a tuple, the only sensible template parameter for get_state<T>() is T returning a T&.
The overload for a T* is removed and the T& is discouraged, although still supported.
If you need to get a state by its address, use the address operator after you have received the state by reference.
The eUML front-end support
The support for EUML induces longer compilation times by the need to include the Boost proto headers and applying C++03 variadic template emulation. If you want to use a UML-like syntax, please try out the new PUML front-end.
Deprecation information
Deprecations of features, APIs, and other changes with additional context are listed in the table below.
| Feature | Deprecation / Removal | Description |
|---|---|---|
Public access to the event container |
1.90 / 1.91 |
The event container can be accessed and manipulated via public APIs. Manipulation of the container outside of the library code can lead to undefined behavior. Change: The public API to access the event container will be changed to |
Renaming of |
1.91 / 1.92 |
The default setting of the Change: The behavior will be corrected to match |
Removal of APIs to process queued events |
1.91 / 1.92 |
The event containers for queued events and deferred events have been merged into a single event pool. The APIs for processing queued events are obsolete. Change: The APIs Occurrences of the old APIs can be replaced as follows:
|
Removal of automatic enqueuing in the |
1.91 / 1.92 |
The API Change: The API Calls to |