boost-ext / sml2

C++20 State Machine library
144 stars 4 forks source link

Usage issue with an object containing all the actions and guards as member functions. #11

Open phelter opened 1 week ago

phelter commented 1 week ago

With state machines I'd like to encapsulate the state machine within an object - after a bunch of investigation and trial and error with sml and sml2 found the following - works best: Something along the lines of this comment: https://github.com/boost-ext/sml/issues/311#issuecomment-548883956

The issue I have is when I try and use lambda's

eg:

#include <sml2>
#include <iostream>
#include <chrono>
#include <fmt/chrono.h>
#include <fmt/format.h>
#include <utility>
#include <thread>

namespace phs = std::placeholders;

struct Wait
{
    std::chrono::seconds duration;
};
struct Cancel
{
};
struct Expire
{
};

static std::string formatTimepoint(const std::chrono::system_clock::time_point &timePoint)
{
    // Convert time_point to time_t
    std::time_t time = std::chrono::system_clock::to_time_t(timePoint);

    // Convert time_t to tm as local time
    std::tm tm = *std::localtime(&time);

    // Format the tm structure to a string using fmt::format
    return fmt::format("{:%Y-%m-%d %H:%M:%S}", tm);
}

class TEBase {
public:
    virtual ~TEBase() = default;
    virtual void start(const Wait &ev) = 0;
};

struct SmCtx
{
    SmCtx(TEBase &t)
        : te(t)
    {}

    auto operator()()
    {
        using namespace sml;
        using namespace sml::dsl;

        // auto start = [this](const auto &ev) -> void { te.start(ev); }; // Fails
        // auto start = [&](const auto &ev) -> void { te.start(ev); }; // Fails
        auto start = std::bind(&TEBase::start, &te, phs::_1);

        return transition_table{*"idle"_s + event<Wait> / start = "waiting"_s};
    }

    TEBase &te;
};

class TE : TEBase {
public:
    TE()
        : m_sm(SmCtx{*this})
    {}
    ~TE() override = default;

    void run() { m_sm.process_event(Wait{std::chrono::seconds(1)}); }

    void start(const Wait &ev) override
    {
        std::cout << "Hello World at: " << formatTimepoint(std::chrono::system_clock::now()) << std::endl;
        std::this_thread::sleep_for(ev.duration);
        std::cout << "Just woke up at: " << formatTimepoint(std::chrono::system_clock::now()) << std::endl;
    }

    sml::sm<SmCtx> m_sm;
};

int main()
{
    TE te;
    te.run();
    return 0;
}

The lambda's don't work since there is a difference in how the lambda captures this vs the std::bind captures this. Both will compile but when using lambda's there's a seg-fault when it tries to call the action that is the lambda.

Would be great if there's a compile-time check that a lambda isn't usable.

Also if concepts could be used to provide much easier to understand error identification when things like this are an issue. The amount of cruff in the compile warnings are problematic for static debugging of the issues related to the state machine.

phelter commented 1 week ago

I might be wrong here, but believe it's because std::bind performs type erasure which is required for the input event type? - Not quite sure why they both compile (using clang-17 ubuntu 22.04.4) but the lambda seg-faults and the bind does not.