关于c++17:谈-C17-里的-State-模式之二

39次阅读

共计 14964 个字符,预计需要花费 38 分钟才能阅读完成。

这是第二局部,无关无限状态机(FSM)的 C++ 实作局部,也等同于状态模式实现

Prologue

上一篇 谈 C++17 里的 State 模式之一 对于状态模式所牵扯到的基本概念做了一个综述性的梳理。所以是时候从这些概念中抽取咱们感兴趣的局部予以实作了。

C++ 实现(元编程实现)

如果不采纳 DFA 实践推动的伎俩,而是在 C++11/17 的语境里思考实现状态模式,那么咱们应该从新梳理一下实践:

  • 状态机 FSM:状态机总是无限的(咱们不可能去解决有限的状态汇合)。
  • 开始状态 S:Start State/Initial State
  • 以后状态 C:Current State
  • 下一状态 N:Next State
  • 终止状态:Terminated State (Optional)
  • 进入状态时的动作:enter-action
  • 来到状态时的动作:exit-action
  • 输出动作 / 输出流:input action,也能够是输出条件、或者事件对象等
  • 转换:Transition
  • 上下文:Context
  • 负载:Payload

有的时候,Input Action 也被称作 Transition Condition/Guard。它的外延始终如一,是指在进入下一状态前通过条件进行断定状态变迁是否被许可。

状态机

外围定义

依据以上的设定,咱们决定了 fsm machine 的根底定义如下:

namespace fsm_cxx {

  AWESOME_MAKE_ENUM(Reason,
                    Unknown,
                    FailureGuard,
                    StateNotFound)

  template<typename S,
           typename EventT = event_t,
           typename MutexT = void, // or std::mutex
           typename PayloadT = payload_t,
           typename StateT = state_t<S>,
           typename ContextT = context_t<StateT, EventT, MutexT, PayloadT>,
           typename ActionT = action_t<S, EventT, MutexT, PayloadT, StateT, ContextT>,
           typename CharT = char,
           typename InT = std::basic_istream<CharT>>
    class machine_t final {
      public:
      machine_t() {}
      ~machine_t() {}
      machine_t(machine_t const &) = default;
      machine_t &operator=(machine_t &) = delete;

      using Event = EventT;
      using State = StateT;
      using Context = ContextT;
      using Payload = PayloadT;
      using Action = ActionT;
      using Actions = detail::actions_t<S, EventT, MutexT, PayloadT, StateT, ContextT, ActionT>;
      using Transition = transition_t<S, EventT, MutexT, PayloadT, StateT, ContextT, ActionT>;
      using TransitionTable = std::unordered_map<StateT, Transition>;
      using OnAction = std::function<void(StateT const &, std::string const &, StateT const &, typename Transition::Second const &, Payload const &)>;
      using OnErrorAction = std::function<void(Reason reason, State const &, Context &, Event const &, Payload const &)>;
      using StateActions = std::unordered_map<StateT, Actions>;
      using lock_guard_t = util::cool::lock_guard<MutexT>;

      // ...
    };
}

这是重复迭代之后的成绩。

你肯定要明确,少数人,和我一样,都是那种脑容量一般的人,咱们做设计时一开始都是简陋的,而后一直修改枝蔓、改善设计后能力失去看起来仿佛还算齐备的后果,如同下面给出的主机器定义那样。

晚期版本

作为一个信念加强,上面给出首次跑通一个事件触发和状态推动时的 machine_t 定义:

    template<typename StateT = state_t,
             typename ContextT = context_t<StateT>,
             typename ActionT = action_t<StateT, ContextT>,
             typename CharT = char,
             typename InT = std::basic_istream<CharT>>
    class machine_t final {
    public:
        machine_t() {}
        ~machine_t() {}

        using State = StateT;
        using Action = ActionT;
        using Context = ContextT;
        using Transition = transition_t<StateT, ActionT>;
        using TransitionTable = std::unordered_map<StateT, Transition>;
        using on_action = std::function<void(State const &, std::string const &, State const &, typename Transition::Second const &)>;
      
      // ...
    };

你得晓得的是,状态机的设计有肯定的复杂度,这个规模不能算大规模,中规模都算不上,然而也不算小。

不会有多少人可能一次性将其设计并编码到位。除非这个人脑容量特大,再不然就是他习惯于首先做齐备的 UML 图,而后 convert to C++ codes…,不过这种性能应该是在 IBM Rational Rose 时代才比拟行得通的步骤了,当初曾经不太可能借助什么 UML 工具这样来做设计了,我不分明 PlantUML 明天的倒退状况,但我本人是很久没有画过 UML 图了,还不如我手写出 classes 来得直观呢,至多对我的脑路是这样的。

阐释

machine_t 的头部定义了一堆模板参数。我感觉无须要什么额定的解释,它们的用意大概是可能直白地传递给你的,如果不能,你可能须要回顾一下状态机的各种背景,嗯,问题相对不会在我身上。

SStateT

须要特地提及的是,S 是 State 的枚举类传入,咱们要求你肯定要在这里传入一个枚举类作为状态表,并且咱们倡议你的枚举类采纳 AWESOME_MAKE_ENUM 宏来帮忙定义(不是必须)。留神在稍后 S 会被 state_t<S> 所装璜,在 machine_t 外部的所有场合,咱们只会应用这个包装过后的类来拜访和操作状态。

这是一种防御性的编程手法。

如果将来咱们想要引入其余机制,例如一个状态类体系而不是枚举类型的值示意,那么咱们能够提供一个不同的 state_t 包装计划,从而将新的机制无毁坏地引入到现有的 machine_t 体系中。甚至于咱们连 state_t 也能够不用毁坏,仅仅是对其做带有 enable_if 的模板特化就足矣。

StateTState

你可能留神到模板参数 StateTusing 别名 State 了:

using State = StateT;

定义别名的用意至多有两个:

  1. 在 machine_t 的外部和内部,应用类型别名 State 比应用 machine_t 的模板参数名要牢靠的多,并且少数时候(尤其是在 machine_t 的内部)你只能应用类型别名
  2. 采纳形象后的类型别名有利于调整调优设计

State 上,咱们能够间接应用 StateT,也能够应用更简单的定义,这些变更(简直)不会影响到 State 的使用者。例如:

using State = std::optional<StateT>;

也是行得通的。当然理论工程中这么做没有什么必要性。

CharTInT

它们会在将来某一时间点有用。

对于吃进字符流并作 DFA 推动的场景它们可能是有用的。

但目前只是停留在念头上。

OnAction 以及 OnErrorAction

OnAction 实际上是 on_transition_made / on_state_changed 的意思。临时来讲咱们没有 rename 令其更显著,因为当初只想着要有一个能够调试输入的 callback,还没有想过 on_state_changed 的 Hook 的必要性。直到起初做了 OnErrorAction 的设计之后才察觉到有必要关联两个 callbacks。

其它定义以及如何应用

状态汇合

有可能有多种形式提供状态汇合,如:枚举量,整数,短字符串,甚至是小型构造。

不过在 fsm-cxx 中,咱们约定你总是定义枚举量作为 fsm machine 的状态汇合。你的枚举类型将作为 machine 的模板参数 S 而传递,machine 将以此为根底进行若干的封装。

指定状态的枚举量汇合

所以应用时的代码相似于这样:

AWESOME_MAKE_ENUM(my_state,
                  Empty,
                  Error,
                  Initial,
                  Terminated,
                  Opened,
                  Closed)

machine_t<my_state, event_base> m;

在 cxx 枚举类型 中咱们已经介绍过 AWESOME_MAKE_ENUM 能够简化枚举类型的定义,在这里你只须要将其看成是:

enum class my_state {
                      Empty,
                      Error,
                      Initial,
                      Terminated,
                      Opened,
                      Closed  
}

就能够了。

设定 states

接下来能够申明一些根本状态:

machine_t<my_state, event_base> m;
using M = decltype(m);

// states
        m.state().set(my_state::Initial).as_initial().build();
m.state().set(my_state::Terminated).as_terminated().build();
m.state().set(my_state::Error).as_error()
  .entry_action([](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cerr << ".. <error> entering" << '\n';})
  .build();
m.state().set(my_state::Opened)
  .guard([](M::Event const &, M::Context &, M::State const &, M::Payload const &) -> bool {return true;})
  .guard([](M::Event const &, M::Context &, M::State const &, M::Payload const &p) -> bool {return p._ok;})
  .entry_action([](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <opened> entering" << '\n';})
  .exit_action([](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <opened> exiting" << '\n';})
  .build();
m.state().set(my_state::Closed)
  .entry_action([](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <closed> entering" << '\n';})
  .exit_action([](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <closed> exiting" << '\n';})
  .build();

Initial 是必须的初始状态。状态机总是呆在这里,直到有信号推动它。初始状态能够用 as_initial() 来授予。

terminated,error 状态是可选的。而且临时来讲它们没有显著的作用——但你能够在你的 action 中检测这些状态并做出相应的应变。相似的,有 as_terminated()/as_eeror() 来实现相应的指定。

对于每一个状态枚举量来说,你能够依据须要为它们关联 entry/exit_action,如同下面的 entry_action()/exit_action() 调用所展现的那样。

guards

对于一个将要转进的 state,你也能够为其定义 guards。和 Transition Guards 雷同地,一个 State guard 是一个能够返回 bool 后果的函数。而且,该 guard 的用处也类似:在将要转进某个 state 时,依据上下文环境做出判断,以决定是否应该转进到该 state 中。返回 false 时转进动作将不会执行。

定义 state guards 的形式如同这样:

// guards
m.state().set(my_state::Opened)
  .guard([](M::Event const &, M::Context &, M::State const &, M::Payload const &) -> bool {return true;})
  .guard([](M::Event const &, M::Context &, M::State const &, M::Payload const &p) -> bool {return p._ok;})

你能够为一个 state 定义多条 guards。在下面的实例中,第二条 guard 将会依据 payload 中携带的 _ok 布尔类型值来决定要不要转进到 Opened 状态。

如果 guard 示意不能够转进,则状态机停留在原地位,machine_t::on_error() 回调函数将会取得一个 reason == Reasion::FailureGuard 的告诉,你能够在此时操纵 context 转进到另一状态,但要留神这时候将会是一个外部操作:通过 context.current(new_state) 进行到外部转进操作是不会触发任何条件束缚和回调机会的。

同样情理,在 guard() 所增加的 guard 函数中你也能够操作 context 去批改新的转进状态而不会触发进一步的条件束缚和回调机会。

事件

事件,或者说步进信号,须要以一个公共的基类 event_t 为基准,event_t 被用作模板参数传递给 fsm machine,所以你能够应用这个默认设定。

你当然也能够传递一个不同的自定义的基类作为模板参数。例如:

struct event_base {};
struct begin : public event_base {};
struct end : public event_base {};
struct open : public event_base {};
struct close : public event_base {};

machine_t<my_state, event_base> m;

但这样的 event 体系有可能过于简略了,并且存在着类型失落的危险(没有虚析构函数申明的类体系是危险的)。

所以咱们倡议你采纳 fsm-cxx 预置的 event_tevent_type<E> 来实现你的事件类体系,也就是这样:

struct begin : public fsm_cxx::event_type<begin> {virtual ~begin() {}
  int val{9};
};
struct end : public fsm_cxx::event_type<end> {virtual ~end() {}};
struct open : public fsm_cxx::event_type<open> {virtual ~open() {}};
struct close : public fsm_cxx::event_type<close> {virtual ~close() {}};

这样裁减之后,也能够免去显式申明 event 模板参数:

machine_t<my_state> m;
// Or
machine_t<my_state, fsm_cxx::event_t> m;

除了下面的益处之外,最大的益处是你能够应用 (begin{}).to_string() 来失去类名。它是依赖 event_tevent_type<E> 的简要包装所提供的撑持:

namespace fsm_cxx {
  struct event_t {virtual ~event_t() {}
    virtual std::string to_string() const { return "";}
  };
  template<typename T>
  struct event_type : public event_t {virtual ~event_type() {}
    std::string to_string() const { return detail::shorten(std::string(debug::type_name<T>())); }
  };
}

这对于将来的腾挪留下了充沛的余地。

如果你感觉为每个事件类写一个虚析构函数太过于弱爆了,那么用一个辅助的宏好了:

struct begin : public fsm_cxx::event_type<begin> {virtual ~begin() {}
  int val{9};
};
FSM_DEFINE_EVENT(end);
FSM_DEFINE_EVENT(open);
FSM_DEFINE_EVENT(closed);

上下文

在 machine_t 中维持一份外部的上下文环境 Context,这在产生状态转换时是十分重要的外围构造。

Context 提供了以后所处的状态地位,并容许你批改该地位。但要留神如果你通过这个能力进行状态批改的话,条件束缚和回调函数将会被你的操作所略过。

如果查看 context_t 的源代码你会发现在这个上下文环境中 fsm-cxx 还治理了和 states 相干的 entry/exit_action 及其校验代码。这个设计原本是为将来的 HFSM 而筹备的。

负载

负载 Payload 从使用者编码的角度来看是游离在上下文、事件之外的。但对于状态机实践来说,它是随着事件一起被传递给状态机的。

在每一次推动状态机步进时,你能够通过 m.step_by() 携带一些有效载荷。这些载荷能够参加 guards 决策,也能够在 entry/exit_actions 中参加动作执行。

默认时 machine_t 应用 payload_t 作为其 PayloadT 模板参数。所以你只须要从 payload_t 派生你的类就能够自定义想要携带的负载了:

struct my_payload: public fsm_cxx::payload_t {};

你也能够采纳 payload_type 模板包装的形式:

struct my_payload: public fsm_cxx::payload_type<my_payload> {// ...}

至于 machine_t 的模板参数无需做什么批改。

应用时通过 m.step_by(event, payload) 间接传递 my_payload 实例即可。

转换表

咱们的实现中筹备简略地建设两级 hash_map,但第二级中应用一种较蠢笨的结构形式。目前看来还没有必要应该在这个部位进行额定的优化。

具体的定义是这样的:

using Transition = transition_t<S, EventT, MutexT, PayloadT, StateT, ContextT, ActionT>;
using TransitionTable = std::unordered_map<StateT, Transition>;

转换表以 from_state 为第一层的 key,并关联一个 transition_t 构造。在 transition_t 中,实际上又有第二级 hash_map 是关联到 EventT 的类名上的,所以一个 EventT 实例信号会索引到关联的 trans_item_t 构造,但这里须要留神的是 EventT 实例自身不重要,重要的是它的类名。你看到咱们之前约定事件信号应该别离以最小型 struct 的形式予以申明,而 struct 的构造体成员是被忽视的,machine_t 只须要它的类型名称。

template<typename S,
         typename EventT = dummy_event,
         typename MutexT = void,
         typename PayloadT = payload_t,
         typename StateT = state_t<S, MutexT>,
         typename ContextT = context_t<StateT, EventT, MutexT, PayloadT>,
         typename ActionT = action_t<S, EventT, MutexT, PayloadT, StateT, ContextT>>
  struct transition_t {
    using Event = EventT;
    using State = StateT;
    using Context = ContextT;
    using Payload = PayloadT;
    using Action = ActionT;
    using First = std::string;
    using Second = detail::trans_item_t<S, EventT, MutexT, PayloadT, StateT, ContextT, ActionT>;
    using Maps = std::unordered_map<First, Second>;

    Maps m_;

    //...
  };

依照上述定义,你在应用时应该这么定义转换表:

// transistions
m.transition(my_state::Initial, begin{}, my_state::Closed)
  .transition(my_state::Closed, open{}, my_state::Opened,
    [](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <closed -> opened> entering" << '\n';},
    [](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <closed -> opened> exiting" << '\n';})
  .transition(my_state::Opened, close{}, my_state::Closed)
  .transition(my_state::Closed, end{}, my_state::Terminated);

m.transition(my_state::Opened,
             M::Transition{end{}, my_state::Terminated,
                           [](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <T><END>" << '\n';},
                           nullptr});

相似于 state(),定义一条转换表规定时,能够为规定挂钩专属的 entry/exit_action,你能够依据你的理论需要来抉择是在 state 还是 transition-rule 的失当地位 hook 事件并执行 action。

你能够抉择采纳 Builder Pattern 的格调来结构转换表条目:

m.builder()
  .transition(my_state::Closed, open{}, my_state::Opened)
  .guard([](M::Event const &, M::Context &, M::State const &, M::Payload const &p) -> bool {return p._ok;})
  .entry_action([](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <closed -> opened> entering" << '\n';})
  .exit_action([](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <closed -> opened> exiting" << '\n';})
  .build();

它和一次 m.transition() 调用是等价的。

Guard for A Transition

在转换表定义时,你能够为一个变换(Transition)定义一个前提条件。在转换将要产生时,状态机将会校验该 guard 期待的条件是否满足,只有在满足时才会执行转换动作。

咱们已经提到过,通过 event 信号面向 from_state 的转换门路能够有多条,理论转进中在多条门路中如何抉择呢?就是通过 guard 条件来筛选的。

在具体实现中,还隐含着程序筛选准则:最先满足 guard 条件的门路被优先择出,后续的门路则被放弃探查。

无限度

一条转换表条目代表着从 from_state 因为事件 ev 的激励而转进到 to_state。咱们既不限度 from 到 (ev, to_state) 的转换门路,而是利用 guard 条件进行抉择(但实际上是一个程序优先的抉择)。具体情况能够参考源码的 transition_t::get() 局部。

推动状态机运行

当上述的次要定义实现之后,状态机就处于可工作状态。此时你须要某种机制来推动状态机运行。例如当接管到一个鼠标事件时,你能够调用 m.step_by() 去推动状态机。如果推动胜利,则状态机将会变换到新的状态。

例如上面的代码做了简略的推动:

m.step_by(begin{});   // goto Closed
if (!m.step_by(open{}, payload_t{false}))
  std::cout << "E. cannot step to next with a false payload\n";
m.step_by(open{});    // goto Opened
m.step_by(close{});
m.step_by(open{});
m.step_by(end{});

其输入后果如同这样:

        [Closed] -- begin --> [Closed] (payload = a payload)
          .. <closed> entering
          Error: reason = Reason::FailureGuard
          E. cannot step to next with a false payload
          .. <closed -> opened> exiting
          .. <closed> exiting
        [Opened] -- open --> [Opened] (payload = a payload)
          .. <closed -> opened> entering
          .. <opened> entering
          .. <opened> exiting
        [Closed] -- close --> [Closed] (payload = a payload)
          .. <closed> entering
          .. <closed -> opened> exiting
          .. <closed> exiting
        [Opened] -- open --> [Opened] (payload = a payload)
          .. <closed -> opened> entering
          .. <opened> entering
          .. <opened> exiting
        [Closed] -- end --> [Closed] (payload = a payload)
          .. <closed> entering

留神推动代码中的第二行会因为 guard 的缘故导致推动不胜利,所以输入行中会有 Error: reason = Reason::FailureGuard 这样的输入信息。

线程平安

如果你须要一个线程平安的状态机,那么能够给 machine_t 传入第三个模板参数为 std::mutex。如同这样:

fsm_cxx::machine_t<my_state, fsm_cxx::event_t, std::mutex> m;
using M = decltype(m);

// Or:
fsm_cxx::safe_machine_t<my_state> m;

在 m.step_by 的外部进行了竞态条件管制。

然而在定义性能中(例如定义 state/guard/transition 的时候)并没有进行爱护,所以线程平安仅实用于 machine_t 开始运行之后。

另外,如果你自定义、或者扩大了你的上下文类,在上下文的外部操作中必须进行竞态条件爱护。

示例代码残缺一览

下面提到的测试用的代码:

namespace fsm_cxx { namespace test {

  // states

  AWESOME_MAKE_ENUM(my_state,
                    Empty,
                    Error,
                    Initial,
                    Terminated,
                    Opened,
                    Closed)

  // events

  struct begin : public fsm_cxx::event_type<begin> {virtual ~begin() {}
    int val{9};
  };
  struct end : public fsm_cxx::event_type<end> {virtual ~end() {}};
  struct open : public fsm_cxx::event_type<open> {virtual ~open() {}};
  struct close : public fsm_cxx::event_type<close> {virtual ~close() {}};

  void test_state_meta() {
    machine_t<my_state> m;
    using M = decltype(m);

    // @formatter:off
    // states
    m.state().set(my_state::Initial).as_initial().build();
    m.state().set(my_state::Terminated).as_terminated().build();
    m.state().set(my_state::Error).as_error()
      .entry_action([](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cerr << ".. <error> entering" << '\n';})
      .build();
    m.state().set(my_state::Opened)
      .guard([](M::Event const &, M::Context &, M::State const &, M::Payload const &) -> bool {return true;})
      .guard([](M::Event const &, M::Context &, M::State const &, M::Payload const &p) -> bool {return p._ok;})
      .entry_action([](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <opened> entering" << '\n';})
      .exit_action([](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <opened> exiting" << '\n';})
      .build();
    m.state().set(my_state::Closed)
      .entry_action([](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <closed> entering" << '\n';})
      .exit_action([](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <closed> exiting" << '\n';})
      .build();

    // transistions
    m.transition().set(my_state::Initial, begin{}, my_state::Closed).build();
    m.transition()
      .set(my_state::Closed, open{}, my_state::Opened)
      .guard([](M::Event const &, M::Context &, M::State const &, M::Payload const &p) -> bool {return p._ok;})
      .entry_action([](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <closed -> opened> entering" << '\n';})
      .exit_action([](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <closed -> opened> exiting" << '\n';})
      .build();
    m.transition().set(my_state::Opened, close{}, my_state::Closed).build()
      .transition().set(my_state::Closed, end{}, my_state::Terminated).build();
    m.transition().set(my_state::Opened, end{}, my_state::Terminated)
      .entry_action([](M::Event const &, M::Context &, M::State const &, M::Payload const &) {std::cout << ".. <T><END>" << '\n';})
      .build();
    // @formatter:on

    m.on_error([](Reason reason, M::State const &, M::Context &, M::Event const &, M::Payload const &) {std::cout << "Error: reason =" << reason << '\n';});

    // debug log
    m.on_transition([&m](auto const &from, fsm_cxx::event_t const &ev, auto const &to, auto const &actions, auto const &payload) {std::printf("[%s] -- %s --> [%s] (payload = %s)\n", m.state_to_sting(from).c_str(), ev.to_string().c_str(), m.state_to_sting(to).c_str(), to_string(payload).c_str());
      UNUSED(actions);
    });

    // processing

    m.step_by(begin{});
    if (!m.step_by(open{}, payload_t{false}))
      std::cout << "E. cannot step to next with a false payload\n";
    m.step_by(open{});
    m.step_by(close{});
    m.step_by(open{});
    m.step_by(end{});

    std::printf("---- END OF test_state_meta()\n\n\n");
  }

}}

Epilogue

这一次,代码的细节太多,所以咱们偏重于解释如何应用 fsm-cxx。并且因为篇幅的起因,也没有足够的地盘提供残缺的代码,所以请参考 repo: https://github.com/hedzr/fsm-cxx。

总的来说,这一次写的本人都不称心。

这种文章总是会十分无趣的吧,不管怎么写都感觉一片散乱。

正文完
 0