boost-ext / sml

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

Injecting exception<_> event in subsequent action #463

Open sdebionne opened 3 years ago

sdebionne commented 3 years ago

For generic exception<_> events, it would be useful to inject the event/exception in the subsequent action. AFAIU, it works for typed exception event such as exception<std::runtime_error> only.

Here is a godbolt that shows the "issue" and a repro:

// $CXX -std=c++14 error_handling.cpp
#include <boost/sml.hpp>
#include <iostream>
#include <stdexcept>

namespace sml = boost::sml;

namespace {

struct exception_handling {
  auto operator()() const {
    using namespace sml;
    return make_transition_table(
        *("idle"_s) + "event1"_e / [] { throw std::runtime_error{"error"}; }
      , *("exceptions handling"_s) + exception<std::runtime_error> / [] (auto const& evt) { std::cout << "exception caught: " << evt.what() << std::endl; }
    );
  }
};
}  // namespace

int main() {
  using namespace sml;
  sm<exception_handling> sm;

  sm.process_event("event1"_e());  // throws runtime_error
}
kielby commented 1 year ago

Does anyone have a suggestion on how to implement something like this?

Schoppenglas commented 1 year ago

First of all, let's recap try-catch-block:

try
{
    f();
}
catch (const std::runtime_error& e)
{
    log(e.what); // this executes if f() throws std::runtime_error (same type rule) -- here, we do have a what() function
}
catch (const std::exception& e)
{
    log(e.what); // this executes if f() throws std::exception (base class rule) -- here, we do have a what() function
}
catch (...)
{
    log("catch_all"); // this executes if f() throws int or any other non-exception type -- here, we do NOT have a what() function
}

with this in common

#include "sml.hpp"

#include <cassert>
#include <iostream>
#include <stdexcept>

namespace sml = boost::sml;

namespace {

struct exception_handling {
  auto operator()() const {
    using namespace sml;

    constexpr auto on_exception = [](const auto& evt) {
        if (std::is_same_v<std::remove_cvref_t<decltype(evt)>, std::runtime_error>) std::cout << "std::runtime_error: " << evt.what() << std::endl;
        else std::cout << evt.what() << std::endl;
    };

    constexpr auto on_exception_catch_all = [](const auto& evt) {
        std::cout << "catch_all" << std::endl;
    };

    return make_transition_table(
        *("idle"_s) + "event1"_e / [] {
            //throw std::logic_error{"ouch2"};
            //throw std::exception{};
            throw std::runtime_error{"ouch"};
            }
      , *("exceptions handling"_s) + exception<_> / on_exception_catch_all = "fault"_s
      , "exceptions handling"_s + exception<std::runtime_error> / on_exception = "fault"_s
      , "exceptions handling"_s + exception<std::exception> / on_exception = "fault"_s

    );
  }
};

}  // namespace

int main() {
  using namespace sml;
  sm<exception_handling> sm;

  sm.process_event("event1"_e());
}

In state "exceptions handling" we have to catch all specializations of std::exception on its own, because else the specializations will be casted to std::exception and we no more information about the type.

See Compiler Explorer Example: https://godbolt.org/z/vved46rah

sdebionne commented 1 year ago

Thanks for looking into this!

Right, we don't have access directly to the exception in a catch all handler, but calling std::current_exception() is an option:

catch (...)
{
   std::exception_ptr p = std::current_exception();
}

How about passing an std::exception_ptr to the subsequent action of exception<_>? Or call std::current_exception() in the on_exception_catch_all action?

constexpr auto on_exception_catch_all = [](const auto& evt) {
        std::exception_ptr p = std::current_exception();
        std::cout <<(p ? p.__cxa_exception_type()->name() : "null") << std::endl;
    };

This last option seems to fit the bill for me, but I am not sure it would work with the defer_queue policy.