原文: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()中的语句。你也可以将此秒表程序扩展为一个交互式的控制台应用。