c++boostboost-msm

Starting state machine cannot handle internal transition on startup


I have the following state machine (sorry, I couldn't find how to make a smaller MRE):

It works perfectly, and the state machine ignores the Trigger event as intended. However, if the on_entry method of MainSM sends the Trigger events then starting the state machine will not handle the event and call no_transition.

What is the problem? Is the SM not ready yet when calling start? Is this a bug or it is in-spec?

Here is the code snippet. Removing the process_event call line 80 and everything work.

#include <iostream>

#include <boost/core/demangle.hpp>
#include <boost/msm/back/state_machine.hpp>
#include <boost/msm/front/state_machine_def.hpp>
#include <boost/msm/front/functor_row.hpp>

#define ON_ENTRY_LOG_NAME(name) \
    template <class Event, class FSM>                   \
    void on_entry(const Event &, FSM&) {            \
        std::cout << "Entering " #name << std::endl;    \
    }
#define ON_EXIT_LOG_NAME(name) \
    template <class Event, class FSM> \
    void on_exit(const Event &, FSM&) { \
        std::cout << "Exitting " #name << std::endl;    \
    }


namespace  // Concrete FSM implementation
{

    namespace msm = boost::msm;
    namespace msmb = boost::msm::back;
    namespace msmf = boost::msm::front;
    namespace mpl = boost::mpl;

    // events
    struct Stop {};
    struct Recover {};
    struct Start {};
    struct Trigger {};

    struct SubSM_front: msmf::state_machine_def<SubSM_front>
    {
        struct Fidgetting: msmf::state<>
        {
            ON_ENTRY_LOG_NAME(Fidgetting);
            ON_EXIT_LOG_NAME(Fidgetting);
        };

        struct FidgettingCompulsively: msmf::state<>
        {
            ON_ENTRY_LOG_NAME(FidgettingCompulsively);
            ON_EXIT_LOG_NAME(FidgettingCompulsively);
        };

        using initial_state = Fidgetting;

        struct transition_table: mpl::vector<
            msmf::Row<Fidgetting,             Trigger, FidgettingCompulsively>,
            msmf::Row<FidgettingCompulsively, Trigger, Fidgetting>
        > {};

        ON_ENTRY_LOG_NAME(SubSM);
        ON_EXIT_LOG_NAME(SubSM);
    };

    using SubSM = msmb::state_machine<SubSM_front>;

    struct MainSM_front: msmf::state_machine_def<MainSM_front>
    {
        struct Default: msmf::state<>
        {
            ON_ENTRY_LOG_NAME(Default);
            ON_EXIT_LOG_NAME(Default);
        };

        using initial_state = Default;

        struct transition_table: mpl::vector<
            msmf::Row<Default, Start, SubSM>
        > {};

        template <class Event, class FSM>
        void on_entry(const Event &, FSM &fsm)
        {
            std::cout << "Entering MainSM" << std::endl;
            // This line make a call to no_transition
            fsm.process_event(Trigger{});
        }

        ON_EXIT_LOG_NAME(MainSM);
    };

    using MainSM = msmb::state_machine<MainSM_front>;

    struct SM_front: msmf::state_machine_def<SM_front>
    {
        struct Stopped: msmf::state<>
        {
            ON_ENTRY_LOG_NAME(Stopped);
            ON_EXIT_LOG_NAME(Stopped);
        };

        using initial_state = MainSM;

        using transition_table = mpl::vector<
            msmf::Row<MainSM,  Stop,    Stopped>,
            msmf::Row<Stopped, Recover, MainSM>
        >;

        using internal_transition_table = mpl::vector<
            msmf::Internal<Trigger>
        >;

        ON_ENTRY_LOG_NAME(SM);
        ON_EXIT_LOG_NAME(SM);
    };

    using SM = msmb::state_machine<SM_front>;

    void test()
    {
        SM sm;

        sm.start();
        sm.process_event(Trigger{});
        sm.stop();
    }
}

int main()
{
    test();
    return 0;
}

Tested with GCC 5.5, Clang 8, Boost 1.58 and 1.73, with C++14.


Solution

  • During initialization of SM, it will initialize the nested MainSM. As part of the initialization it will send InitEvent, which you handle to process TriggerEvent on the containing SM instance.

    IOW, You're processing an event on the fsm parameter, which is the exact same instance as the SM surrounding state machine that was in the process of being initialized.

    I think that's just not supported. Actually, the Trigger is handled "just fine", but after your on_entry handler exits, SM runs into trouble:

     struct direct_event_start_helper
     {
         direct_event_start_helper(library_sm* self_):self(self_){}
         // this variant is for the standard case, entry due to activation of the containing FSM
         template <class EventType,class FsmType>
         typename ::boost::disable_if<typename has_direct_entry<EventType>::type,void>::type
             operator()(EventType const& evt,FsmType& fsm, ::boost::msm::back::dummy<0> = 0)
         {
             (static_cast<Derived*>(self))->on_entry(evt,fsm);
             self->internal_start(evt);
         }
    

    That self->internal_start(evt) doesn't work anymore because the outer SM was manipulated. The assertion

    sotest: boost_1_72_0/boost/msm/front/state_machine_def.hpp:203: void boost::msm::front::state_machine_def<Derived, BaseState>::no_transition(const Event&, FSM&, int) [with FSM = boost::msm::back::state_machine<{anonymous}::MainSM_front>; Event = {anonymous}::Trigger; Derived = {anonymous}::MainSM_front; BaseState = boost::msm::front::default_base_state]: Assertion `false' failed.
    

    indeed means there was no transition:

    template <class FSM,class Event>
    void no_transition(Event const& ,FSM&, int )
    {
        BOOST_ASSERT(false);
    }
    

    DOC FIND

    In the documentation I stumbled on this wording which seems to confirm all of the above:

    Note: you might have noticed that the tutorial calls start() on the state machine just after creation. The start method will initiate the state machine, meaning it will activate the initial state, which means in turn that the initial state's entry behavior will be called. The reason why we need this will be explained in the back-end part. After a call to start, the state machine is ready to process events. The same way, calling stop() will cause the last exit actions to be called.

    (emphasis mine)