原文: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()中的語句。你也可以將此秒表程序擴展為一個交互式的控制臺應用。