State Machine Actions#
Actions are returned from state event handlers and are the normal way of communicating back to the state machine what to do next. The only requirement on an Action type is to have an Execute() method, that gets the state machine instance, the current state and the event being processed.
Additional type management utility methods are added to action classes as needed to simplify the unit testing of the states and actions. They are in no way used in the basic functioning of a state machine which totally relies on static compile-time types and rules.
-
template<typename TargetState>
struct TransitionTo# An action type that represents a state transition from the current state to the target state specified as a template parameter.
This action supports rich states which may contain data and may have some complex logic that utilizes that data to decide on the next state transition. When being executed by the state machine, it is provided with the current state (through the state machine instance), the newly selected state and the event that triggered the transition.
Furthermore, it calls the
OnLeave
method on the previous state andOnEnter
on the destination state, provided they are in the states interface (it uses SFINAE to detect if such a method exists).The requirement on the
OnLeave
method is to return aState
object that can only be one ofContinue
,Terminate
orTerminateWithError
. TheOnEnter
method on the other hand can return any of the possible values ofStatus
.Example
A state can implement astruct TransitionEvent {}; struct ExampleMockState { // NOLINTNEXTLINE MOCK_METHOD(Status, OnEnter, (const TransitionEvent &), ()); // NOLINTNEXTLINE MOCK_METHOD(Status, OnLeave, (const TransitionEvent &), ()); }; struct FirstState; struct SecondState; using Machine = StateMachine<FirstState, SecondState>; struct FirstState : Will<On<TransitionEvent, TransitionTo<SecondState>>> { explicit FirstState(std::shared_ptr<ExampleMockState> mock) : mock_{std::move(mock)} { } // We only implement the OnLeave method, but that's ok. // The action will only call whatever is implemented. [[nodiscard]] auto OnLeave(const TransitionEvent &event) const -> Status { return mock_->OnLeave(event); } private: const std::shared_ptr<ExampleMockState> mock_; }; struct SecondState : Will<ByDefault<DoNothing>> { explicit SecondState(std::shared_ptr<ExampleMockState> mock) : mock_{std::move(mock)} { } // We only implement the OnEnter method, but that's ok. // The action will only call whatever is implemented. [[nodiscard]] auto OnEnter(const TransitionEvent &event) const -> Status { return mock_->OnEnter(event); } private: const std::shared_ptr<ExampleMockState> mock_; }; const auto mock_initial_state = std::make_shared<ExampleMockState>(); const auto mock_another_state = std::make_shared<ExampleMockState>(); Machine machine{ FirstState{mock_initial_state}, SecondState{mock_another_state}}; TransitionEvent event{}; EXPECT_CALL(*mock_initial_state, OnLeave(Ref(event))) .Times(1) .WillOnce(Return(Continue{})); EXPECT_CALL(*mock_another_state, OnEnter(Ref(event))) .Times(1) .WillOnce(Return(Status{Continue{}})); machine.Handle(event); ASSERT_THAT(machine.IsIn<SecondState>(), IsTrue());
Handle
method that returns a TransitionTo object constructed with a piece of data (std::any object) that will be passed to the next state when itsonEnter
method is called. The next state can opt to ignore that data by implementingonEnter(Event event)
only or can opt to use the data by implementingonEnter(Event event, std::any data)
.Example of data passing between states
struct TransitionEvent {}; struct ExampleMockState { // NOLINTNEXTLINE MOCK_METHOD(Status, OnEnter, (const TransitionEvent &, int), ()); // NOLINTNEXTLINE MOCK_METHOD(Status, OnLeave, (const TransitionEvent &), ()); }; struct FirstState; struct SecondState; using Machine = StateMachine<FirstState, SecondState>; struct FirstState { explicit FirstState(std::shared_ptr<ExampleMockState> mock) : mock_{std::move(mock)} { } // We only implement the OnLeave method, but that's ok. // The action will only call whatever is implemented. [[nodiscard]] auto OnLeave(const TransitionEvent &event) const -> Status { return mock_->OnLeave(event); } // This special handler allows for passing data via the // transition object to the next state which can be consumed // if that state implements `onEnter(event, data)` [[nodiscard]] static auto Handle(const TransitionEvent & /*event*/) -> TransitionTo<SecondState> { return TransitionTo<SecondState>{1}; } private: const std::shared_ptr<ExampleMockState> mock_; }; struct SecondState : Will<ByDefault<DoNothing>> { explicit SecondState(std::shared_ptr<ExampleMockState> mock) : mock_{std::move(mock)} { } // This implementation expects data to be passed from the // previous state. [[nodiscard]] auto OnEnter( const TransitionEvent &event, std::any data) const -> Status { EXPECT_THAT(std::any_cast<int>(data), Eq(1)); // Mocking methods with std::any arguments will fail to compile with clang // and there is no fix for it from gmock. Just specifically cast the // std::any before forwarding to the mock. return mock_->OnEnter(event, std::any_cast<int>(data)); } private: const std::shared_ptr<ExampleMockState> mock_; }; const auto mock_initial_state = std::make_shared<ExampleMockState>(); const auto mock_another_state = std::make_shared<ExampleMockState>(); Machine machine{ FirstState{mock_initial_state}, SecondState{mock_another_state}}; TransitionEvent event{}; EXPECT_CALL(*mock_initial_state, OnLeave(Ref(event))) .Times(1) .WillOnce(Return(Continue{})); EXPECT_CALL(*mock_another_state, OnEnter(Ref(event), ::testing::_)) .Times(1) .WillOnce(Return(Status{Continue{}})); machine.Handle(event); ASSERT_THAT(machine.IsIn<SecondState>(), IsTrue());
-
struct DoNothing#
Helper action representing the situation where an event is ignored and the currents state stays as is.
Example
A typical use of this action is in combination with ByDefault to make a state that by default does nothing with any event it receives and therefore only focus on implementing those event handlers that make sense for that particular state.struct DoNothingEvent {}; struct TestState { static auto Handle(const DoNothingEvent & /*event*/) -> DoNothing { // Returning the `DoNothing` action will result in no side effects // from the state machine calling the `Execute` method of the action. return DoNothing{}; } }; StateMachine<TestState> machine{TestState{}}; machine.Handle(DoNothingEvent{});
-
struct ReportError#
The action to be used to report an error from an event handler.
Throwing an exception from event handlers to report an error is not recommended as it will break the interface of the event handlers (expected to return an action to be executed) and make the unit testing more complicated.
Instead, the event handler would use this action to report the error, providing the error explanatory message in the form of the action’s data.
-
template<typename ...Actions>
struct OneOf# Base type for states modeling the situation where the result of an event can be one of a set of possible actions.
Since actions are returned from state
Handle
methods and can have only one return type, this class multiplexes the return types using the variadic template parameter pack.Example
struct SpecialEvent {}; struct FirstState; struct SecondState; using Machine = StateMachine<FirstState, SecondState>; struct SecondState : Will<ByDefault<DoNothing>> {}; struct SpecialAction { static auto Execute(Machine & /*machine*/, FirstState & /*state*/, const SpecialEvent & /*event*/) -> Status { return Continue{}; } }; struct FirstState { explicit FirstState(bool transition) : transition_{transition} { } // This handler has two alternate paths. We use the `OneOf` // helper to still be able to return a single action type [[nodiscard]] auto Handle(const SpecialEvent & /*event*/) const -> OneOf<TransitionTo<SecondState>, SpecialAction> { if (transition_) { return TransitionTo<SecondState>{}; } return SpecialAction{}; } const bool transition_; }; Machine machine1{FirstState{true}, SecondState{}}; try { machine1.Handle(SpecialEvent{}); } catch (...) { } ASSERT_THAT(machine1.IsIn<SecondState>(), IsTrue()); Machine machine2{FirstState{false}, SecondState{}}; machine2.Handle(SpecialEvent{}); ASSERT_THAT(machine2.IsIn<FirstState>(), IsTrue());
-
template<typename Action>
struct Maybe : public asap::fsm::OneOf<Action, DoNothing># Base type for states modeling the situation where when an event is triggered, we want to either do one thing or do nothing at all.
Example
struct SpecialEvent {}; struct FirstState; struct SecondState; using Machine = StateMachine<FirstState, SecondState>; struct SecondState : Will<ByDefault<DoNothing>> {}; struct SpecialAction { static auto Execute(Machine & /*machine*/, FirstState & /*state*/, const SpecialEvent & /*event*/) -> Status { return Continue{}; } }; struct FirstState { explicit FirstState(bool transition) : transition_{transition} { } // This handler has two alternate paths. We use the `OneOf` // helper to still be able to return a single action type [[nodiscard]] auto Handle(const SpecialEvent & /*event*/) const -> OneOf<TransitionTo<SecondState>, SpecialAction> { if (transition_) { return TransitionTo<SecondState>{}; } return SpecialAction{}; } const bool transition_; }; Machine machine1{FirstState{true}, SecondState{}}; try { machine1.Handle(SpecialEvent{}); } catch (...) { } ASSERT_THAT(machine1.IsIn<SecondState>(), IsTrue()); Machine machine2{FirstState{false}, SecondState{}}; machine2.Handle(SpecialEvent{}); ASSERT_THAT(machine2.IsIn<FirstState>(), IsTrue());
-
template<typename T>
struct is_one_of : public std::false_type# A concept to identify if a particular type is a OneOf<…Actions> type.