boost-ext / sml

C++14 State Machine library
https://boost-ext.github.io/sml
Boost Software License 1.0
1.16k stars 179 forks source link

Added policy that controls if the transition table class is instantiated #608

Closed devzeb closed 10 months ago

devzeb commented 10 months ago

Added dont_instantiate_statemachine_class policy: when supplied, don't subclass the transition table type and require a reference to an instance of that type in the constructor of sml::sm and all sub-statemachine types.

Problem:

A non-empty transition table class is always instantiated by boost::sml::sm. This is because sm_impl is a subclass of the transition table class whenever the class is non-empty.

struct sm_impl : aux::conditional_t<aux::is_empty<typename TSM::sm>::value, aux::none_type, typename TSM::sm> {...}

When the transition table class has a non default constructor, it is required to supply a reference to an instance of this class in the constructor of boost::sml::sm. This leads to confusion, when a reference to the instance of a transition table is supplied as a constructor parameter, which is then modified after the creation of the state machine:

struct e1 {};
struct StateMachine {
    explicit StateMachine(int a) {  }

    auto operator()() {
        using namespace sml;
        return make_transition_table(
                // clang-format off
            *"start"_s + event<e1>/ [this]() {
                std::cout << "member_variable: " << member_variable << '\n';
            }
            = "end"_s
                // clang-format on
        );
    }

    int member_variable = 0;
};

StateMachine transition_table_class_instance{0};

sml::sm<StateMachine> sm{
    transition_table_class_instance // copy is made here
};

transition_table_class_instance.member_variable = 42; // this change of the member variable is not reflected in the state machine

sm.process_event(e1{});

This prints:

member_variable: 0

It is not apparent that the assignment of transition_table_class_instance.member_variable does not change the internal state of the state machine, because the normal user does not know that the class is copied.

Solution:

Effects:

The following code is not able to compile:

sml::sm<StateMachine, sml::dont_instantiate_statemachine_class> sm{};

This yields the following error message:

static assertion failed due to requirement '!(should_not_instantiate_statemachine_class<boost::sml::back::sm_policy<StateMachine, boost::sml::back::policies::dont_instantiate_statemachine_class>>::value && aux::would_instantiate_missing_ctor_parameter())': 
When policy sml::dont_instantiate_statemachine_class is used, you have to provide a reference to an instance of the transition table type (boost::sml::sm< your_transition_table_type >) as well as a reference to instances of all sub-statemachine types as constructor parameters.

The previous code example :

StateMachine transition_table_class_instance{0};

sml::sm<StateMachine, sml::dont_instantiate_statemachine_class> sm{
    transition_table_class_instance // no copy is made here
};

transition_table_class_instance.member_variable = 42; // this changes the member variable in the state machine

sm.process_event(e1{});

Now prints:

member_variable: 42

This also enables the state machine to be used in a more object oriented use case like this:

class ClassWithStateMachine final : StateMachine {
  boost::sml::sm<StateMachine, sml::dont_instantiate_statemachine_class> sm{
          static_cast<StateMachine&>(*this)
  };
}

This way, the subclass of the transition table ClassWithStateMachine can easily access and modify protected members of the state machine, that should not be accessible to others (information hiding).

Issue: #607

Reviewers: @