Boost 狀態圖庫--初級篇

原文:The Boost Statechart Library
譯者:penghuster

基本主題:秒表

接下來我們將創建一個簡單的狀態機---秒表。此秒表有兩個按鈕:
-開始/停止
-復位
以及兩種狀態:

  • 停止:指針位于其最后停止的位置,此時秒表處于停止狀態。按復位按鈕可以復位指針到最初位置,按開始/停止按鈕秒表將轉變為運行狀態。
  • 運行:指針處于移動中,連續顯示時間地流逝。按復位按鈕將導致指針移動到最初位置,秒表狀態轉變為停止狀態,按開始/停止按鈕秒表狀態將轉變為停止狀態。
    如下UML圖顯示了秒表的狀態變化:

定義狀態和事件

兩個按鈕是兩個事件的建模。而且,我們也定義了兩個必需的狀態和初始狀態。下面代碼是我們的入口點,隨后的代碼片段將被插入:

#include <boost/statechart/event.hpp>
#include <boost/statechart/state_machine.hpp>
#include <boost/statechart/simple_state.hpp>

namespace sc = boost::statechart;

struct EvStartStop : sc::event< EvStartStop > {};
struct EvReset : sc::event< EvReset > {};

struct Active;
struct StopWatch : sc::state_machine< StopWatch, Active > {};

struct Stopped;

// simple_state 類模板最多可以接收 4 個參數:第三個參數指定內部初始狀態,
//在這里,僅僅 Active有內部狀態,該狀態就是需要傳入到其基類的內部初始狀態。
//第四個參數指定是否有某種歷史狀態需要保持
// Active 是最外層的狀態, 因此需要傳輸其所屬狀態機
struct Active : sc::simple_state<Active, StopWatch, Stopped > {};

// Stopped 和Running 都指定 Active 為其上下文,這將使這兩個狀態內嵌于 Active中
struct Running : sc::simple_state< Running, Active > {};
struct Stopped : sc::simple_state< Stopped, Active > {};

//由于狀態上下文必須一個完整的類型(例如,不允許前置聲明), 
//狀態機需要由外而內定義。我們總是從最外層開始定義,我們也可以寬度優先或深度優先方式定義

int main()
{
  StopWatch myWatch;
  myWatch.initiate();
  return 0;
}

此段代碼可以編譯,執行無任何可觀測結果。

增加事件動作

目前我們將僅使用一種類型的動作:轉變(transitions)。我們插入如下代碼:

#include <boost/statechart/transition.hpp>

struct Stopped;
struct Active : sc::simple_state< Active, StopWatch, Stopped >
{
  typedef sc::transition< EvReset, Active > reactions;
};

struct Running : sc::simple_state< Running, Active >
{
  typedef sc::transition< EvStartStop, Stopped > reactions;
};

struct Stopped : sc::simple_state< Stopped, Active >
{
  typedef sc::transition< EvStartStop, Running > reactions;
};

int main()
{
    StopWatch myWatch;
    myWatch.initiate();
    myWatch.process_event( EvStartStop() );
    myWatch.process_event( EvStartStop() );
    myWatch.process_event( EvStartStop() );
    myWatch.process_event( EvReset() );
    return 0;
}

現在我們所有的狀態和所有的轉變,并且大量的事件都會被發送到秒表。狀態機將嚴格按照期望進行轉變,但是動作并沒有執行。
一個狀態能夠任意數量的動作??梢詫⒉煌膭幼鞣诺?mpl::list<>中,具體參考為一個狀態指定多個動作

局部狀態存儲

接下來我們將使用秒表實際測量時間.不同的秒表狀態所需要的變量也不一樣:

  • Stopped:一個存儲逝去時間的變量
  • Running:一個存儲逝去時間的變量和一個存儲秒表上一次開始時間的變量.
    從上面可以看出,不論秒表處于何種狀態,都需要一個存儲逝去時間的變量.而且,在狀態機收到一個 EvReset 事件時,此變量應該置零.其它的變量僅僅在秒表處于Running狀態下需要.在每次進入Running狀態時,該變量都應該設置為當前系統事件.一旦退出Running狀態時,僅僅需要用當前系統時間減去開始時間,并增加其結果到逝去事件上.
#include <ctime>

// ...

struct Stopped;
struct Active : sc::simple_state< Active, StopWatch, Stopped >
{
  public:
    typedef sc::transition< EvReset, Active > reactions;

    Active() : elapsedTime_( 0.0 ) {}
    double ElapsedTime() const { return elapsedTime_; }
    double & ElapsedTime() { return elapsedTime_; }
  private:
    double elapsedTime_;
};

struct Running : sc::simple_state< Running, Active >
{
  public:
    typedef sc::transition< EvStartStop, Stopped > reactions;

    Running() : startTime_( std::time( 0 ) ) {}
    ~Running()
    {
     // 類似于子類對象訪問父類對象的成員.context<>()可以用于可以直接
     //或間接獲得直接或間接的訪問狀態上下文.此方法也可以直接或間接用
     //于外部狀態或這狀態機本身.// (例如 context< StopWatch >()).
      context< Active >().ElapsedTime() += std::difftime( std::time( 0 ), startTime_ );
    }
  private:
    std::time_t startTime_;
};

// ...

現在該機器可以測量時間,但是在主程序中還不能獲取其結果.

此時,局部狀態存儲的優勢可能還沒有體現出來,更多優勢可以參見"What's so cool about state-local storage?",該文試圖通過與不使用局部狀態存儲的秒表的比較來說明更多的細節.

從機器外部獲得狀態信息

為了獲取這個測量時間,需要一個從外部獲取機器狀態信息的方式.按照目前機器設計有兩種方式可以執行此任務.為了簡單起見,這里使用效率較低的一種方式:state_cast<>()(StopWatch2.cpp 顯示了更細微和復雜的可選方式).從名字看來,其語義與dynamic_cast是十分相似的。例如,當調用myWatch.state_cast< const Stopped & >() 并且此機器當前處于 Stopped狀態,我們能夠獲取 Stopped 狀態的引用;若此時機器不是 stopped 狀態則會拋出異常 std::bad_cast。接著就可以使用此功能來實現通過stopwatch的成員函數返回逝去時間。然而,寧愿請求機器當前處于的狀態,然后根據狀態來計算逝去時間,我們將計算逝去時間的計算放在 Stopped 和 Running狀態中,并通過一個接口來獲取逝去時間。

#include <iostream>

struct IElapsedTime
{
  virtual double ElapsedTime() const = 0;
};

struct Active;
struct StopWatch : sc::state_machine< StopWatch, Active >
{
  double ElapsedTime() const
  {
    return state_cast< const IElapsedTime & >().ElapsedTime();
  }
};

struct Running : IElapsedTime,
  sc::simple_state< Running, Active >
{
  public:
    typedef sc::transition< EvStartStop, Stopped > reactions;

    Running() : startTime_( std::time( 0 ) ) {}
    ~Running()
    {
      context< Active >().ElapsedTime() = ElapsedTime();
    }

    virtual double ElapsedTime() const
    {
      return context< Active >().ElapsedTime() +
        std::difftime( std::time( 0 ), startTime_ );
    }
  private:
    std::time_t startTime_;
};

struct Stopped : IElapsedTime,
  sc::simple_state< Stopped, Active >
{
  typedef sc::transition< EvStartStop, Running > reactions;

  virtual double ElapsedTime() const
  {
    return context< Active >().ElapsedTime();
  }
};

int main()
{
  StopWatch myWatch;
  myWatch.initiate();
  std::cout << myWatch.ElapsedTime() << "\n";
  myWatch.process_event( EvStartStop() );
  std::cout << myWatch.ElapsedTime() << "\n";
  myWatch.process_event( EvStartStop() );
  std::cout << myWatch.ElapsedTime() << "\n";
  myWatch.process_event( EvStartStop() );
  std::cout << myWatch.ElapsedTime() << "\n";
  myWatch.process_event( EvReset() );
  std::cout << myWatch.ElapsedTime() << "\n";
  return 0;
}

為了真實看到時間測量,你可能需要單步調試man()中的語句。你也可以將此秒表程序擴展為一個交互式的控制臺應用。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 又來到了一個老生常談的問題,應用層軟件開發的程序員要不要了解和深入學習操作系統呢? 今天就這個問題開始,來談談操...
    tangsl閱讀 4,164評論 0 23
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,881評論 18 139
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,360評論 11 349
  • 我是上大學之后明顯感覺到我有焦慮癥。 上學期競選了很多職位,都失敗。之前也說過了。嗯,這學期開始補選,我以為我會很...
    暖在手心的皮卡丘閱讀 111評論 7 2
  • 已進九月,盤點了一下,自今年1月1日起,給自己定的小目標,日寫一篇,最多一次間隔了5天沒寫,平均下來一周5篇也還是...
    吳佟閱讀 244評論 0 2