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

C++ exceptions and autotransitions #314

Open davidfrulm opened 4 years ago

davidfrulm commented 4 years ago
#include <boost/sml.hpp>
#include <iostream>
#include <exception>

namespace sml = boost::sml;

enum class Pizza { Margarita, Diavolo };
std::ostream& operator<<(std::ostream& os, const Pizza& pizza)
{
    if (pizza == Pizza::Margarita) { os << "Margarita"; }
    if (pizza == Pizza::Diavolo) { os << "Diavolo"; }
    return os;
}

// Guards
auto isDiavolo = [](const Pizza& pizza) {
  std::cout << "Is Pizza " << pizza << " Diavolo?" << std::endl;
  return pizza == Pizza::Diavolo;
};

auto isMargarita = [](const Pizza& pizza) {
  std::cout << "Is Pizza " << pizza << " Margarita?" << std::endl;

  // Throws first time guard is called
  static int isMargaritaThrowcount = 0;
  if (0 == isMargaritaThrowcount++) throw std::exception{};
  return pizza == Pizza::Margarita;
};

// Actions
auto processPizzaOrder = [] (auto event, Pizza& pizza){
    std::cout << "Received new order for Pizza: " << pizza << std::endl;
};

auto prepareBaseWithCheese = [] (){
    std::cout << "Prepare base with cheese" << std::endl;
};

auto addExtraPeperoni = [] (){
    std::cout << "Custom ingredient for Diavolo Pizza: Peperoni" << std::endl;
    throw std::exception{};
};

auto addExtraCheese = [] (){
    std::cout << "Custom ingredient for Margarita Pizza: Extra Cheese" << std::endl;
};

auto putIntoOven = [] (const Pizza& pizza){
    std::cout << "Pizza " << pizza << " is in the Oven" << std::endl;
};

auto pizzaIsReady = [] (const Pizza& pizza){
    std::cout << "Pizza " << pizza << " is ready!" << std::endl;
};

auto somethingWrongHappened = [] (){
    std::cout << "Something wrong happened..." << std::endl;
};

struct exceptions_in_anonymous_transitions {
    auto operator()() const noexcept {
        using namespace boost::sml;

        return make_transition_table(
            * "SomethingWrongHappened"_s + exception<_> / somethingWrongHappened
            ,
            * "ProcessPizzaOrder"_s / processPizzaOrder = "PrepareBaseWithCheese"_s
            , "PrepareBaseWithCheese"_s / prepareBaseWithCheese = "AddCustomIngredient"_s
            , "AddCustomIngredient"_s + exception<_> / somethingWrongHappened // Added just to prove that "exception" is injected in state
            , "AddCustomIngredient"_s [isDiavolo] / addExtraPeperoni = "PutIntoOven"_s
            , "AddCustomIngredient"_s [isMargarita] / addExtraCheese = "PutIntoOven"_s
            , "PutIntoOven"_s / putIntoOven = "PizzaReady"_s
            , "PizzaReady"_s / pizzaIsReady = X
        );
    }
};

int main()
{
    Pizza pizza1 = Pizza::Diavolo;
    boost::sml::sm<exceptions_in_anonymous_transitions> sm1(pizza1);

    std::cout << std::endl;

    Pizza pizza2 = Pizza::Margarita;
    boost::sml::sm<exceptions_in_anonymous_transitions> sm2(pizza2);
}

Output is as follows:

Received new order for Pizza: Diavolo Prepare base with cheese Is Pizza Diavolo Diavolo? Custom ingredient for Diavolo Pizza: Peperoni Something wrong happened... Pizza Diavolo is in the Oven Pizza Diavolo is ready!

Received new order for Pizza: Margarita Prepare base with cheese Is Pizza Margarita Diavolo? Is Pizza Margarita Margarita? Something wrong happened... Something wrong happened... Is Pizza Margarita Diavolo? Is Pizza Margarita Margarita? Custom ingredient for Margarita Pizza: Extra Cheese Pizza Margarita is in the Oven Pizza Margarita is ready!

From this, what I understand is the following:

I think does not look ok. If an exception happens in an state, there should no transition to next state.

Only way I see to prevent this behaviour is adding an entry at the beginning of each state with auto-transitions to handle sml::exception<_> which contains a transition to other state [e.g.: X]. But that looks quite... unelegant to me. Seems like quite some code duplication if I have several states with auto transitions. Is somebody aware of some other alternative?

madf commented 4 years ago

Both traces look logical to me. An action is just a 'side effect' of a transition. If we have a state and an event - the transition just happens. Actions do not participate in transitions.

On the other hand, a guard may allow or block a transition. The transition only happens when preconditions are met (state and event) and the guard is satisfied. When the guard throws an exception, it's obvious that it is not satisfied, so the transition does not happen.

Exceptions in this context are just another kind of events, so they combine with the state in which they happen.

A side note: it's better not to use exceptions for flow control, they are for exceptional situations that your program cannot handle on the current level of abstraction.

davidfrulm commented 4 years ago

An action is just a 'side effect' of a transition

Moving into target state also is a side-efect of a transition. In fact, I would imagine that only after executing all parts of a transition [current state + event [guard] / action], we are in a possition to safelly move into the next state. Otherwise, "state" of next state is simply undefined.

A side note: it's better not to use exceptions for flow control, they are for exceptional situations that your program cannot handle on the current level of abstraction.

But isn't that what SML does here? It tries to handle exceptional situations by injecting an event as a mechanism of flow control? Please note that problem in the second case is precisely that extra event [not the exception]. That event is the reason why the auto transitions entries are executed.

For this second case: I understand that a "good" solution might not be easy even to draft but, at the very least, is there any way I can model exceptions and auto transitions so that I do not have to add a "Current state + exception<_> / cleanupAction = X" in every state?

madf commented 4 years ago

Here is what the documentation says:

Exception Safety

[Boost].SML doesn't use exceptions internally and therefore might be compiled with -fno-exceptions. If guard throws an exception State Machine will stay in a current state. If action throws an exception State Machine will be in the new state Exceptions might be caught using transition table via exception event. See Error handling.

https://boost-experimental.github.io/sml/overview.html#exception-safety

davidfrulm commented 4 years ago

The problem is that the exception handling in the examples [orthogonal region with an entry in the transition table with + exception<_>] is not really applicable for autotransitions.

In fact, example might be actually rather missleading for no autotrasition cases. If I am accurate, in states where some "currentState + event<_> ..." entries are defined, what really happens is that, after exception is thrown and exception<_> event is injected in the FSM, it will be discarded as an unexpected event.

That means that, unless exception<_> event is set in the state itself, FSM will most likelly have an inconsistent state.

e.g.: I modified previous example to have an "OvenOver" event:

struct exceptions_in_anonymous_transitions {
    auto operator()() const noexcept {
        using namespace boost::sml;

        return make_transition_table(
            * "ProcessPizzaOrder"_s / processPizzaOrder = "PrepareBaseWithCheese"_s
            , "PrepareBaseWithCheese"_s / prepareBaseWithCheese = "AddCustomIngredient"_s
            , "AddCustomIngredient"_s + exception<_> / somethingWrongHappened // Added just to prove that "exception" is injected in state
            , "AddCustomIngredient"_s [isDiavolo] / addExtraPeperoni = "PutIntoOven"_s
            , "AddCustomIngredient"_s [isMargarita] / addExtraCheese = "PutIntoOven"_s
            , "PutIntoOven"_s / putIntoOven = "PizzaReady"_s
            , "PizzaReady"_s + event<OvenOver> / pizzaIsReady = X
        );
    }
};

int main()
{
    Pizza pizza1 = Pizza::Diavolo;
    boost::sml::sm<exceptions_in_anonymous_transitions> sm1(pizza1);
    sm1.process_event(OvenOver{});
}

Outcome is as follows:

Received new order for Pizza: Diavolo Prepare base with cheese Is Pizza Diavolo Diavolo? Custom ingredient for Diavolo Pizza: Peperoni Pizza Diavolo is in the Oven

Please note that there is no trace of the exception thrown in "addExtraPeperoni" action anywhere. Final state of the Pizza is actually undefined.

My current understanding up to now is as follows:

In neither case, a generic solution [e.g.: orthogonal state as in the examples] seems not feasible.

Is this accurate?

madf commented 4 years ago

From here: https://boost-experimental.github.io/sml/tutorial.html#8-handle-errors

When exceptions are enabled (project is NOT compiled with -fno-exceptions) they can be caught using exception syntax. Exception handlers will be processed in the order they were defined, and exception<> might be used to catch anything (equivalent to catch (...)). Please, notice that when there is no exception handler defined in the Transition Table, exception will not be handled by the State Machine.

SML deals with exceptions in two ways. If you use sml::exception<T> in your transition table it wraps all event processing in try/catch and turns all exceptions into sml::exception<T> event, and handles it as an ordinary event. If you don't use sml::exception<T> in your transition table SML doesn't wrap event processing in try/catch and does not handle it in any way. In this case it is all up to you how to deal with it.

Event processing works as follows:

  1. Event injected.
  2. Check guards.
  3. Fire on_exit, switch to a new state, fire on_entry, execute action.

In your first example exception from addExtraPeperoni for Pizza::Diavolo happened when the fsm was in PutIntoOven state, so it was processed in the other orthogonal state and you had only one 'Something wrong happened...'. The exception from isMargarita for Pizza::Margarita happened in the AddCustomIngredient state, so it was processed by both orthogonal states and you ended up with two 'Something wrong happened...'.

I took your modified example and the output is different from yours:

Received new order for Pizza: Diavolo
Prepare base with cheese
Is Pizza Diavolo Diavolo?
Custom ingredient for Diavolo Pizza: Peperoni
Pizza Diavolo is in the Oven
Pizza Diavolo is ready!

The final state is terminate (X). There is no trace of exception here because it is not processed in the PutIntoOven and is discarded. However if I remove the sml::exception<_> from the transition table, the exception is not turned into event and goes through:

Received new order for Pizza: Diavolo
Prepare base with cheese
Is Pizza Diavolo Diavolo?
Custom ingredient for Diavolo Pizza: Peperoni
terminate called after throwing an instance of 'std::exception'
  what():  std::exception

If you want to do a generic cleanup on exceptions - either use sml::exception only in an orthogonal state and do all the cleanup there, or don't use sml::exception at all and wrap event injection (or in case of automatic events - fsm creation) into try/catch and do the cleanup in the catch block.