談 C++17 裡的 State 模式之二

hedzr發表於2021-10-02
這是第二部分,有關有限狀態機(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

總的來說,這一次寫的自己都不滿意。

這種文章總是會非常無趣的吧,不管怎麼寫都覺得一片散亂。

相關文章