boost statechart
接触了boost的状态机,发现不是想象中的那么好用,在一些地方还得用上mpl库里的东西,由于对模板元编程不是很熟练,搞了好些天才算弄明白这该死的mpl::list的原理和用法。
boost的状态机是属于静态链接的状态机,也就是说,它的图结构是编译期间就确定了的,在运行时不可以动态配置。所以,它的用途是有一定局限性的,但在一般情况下,它不仅很通用,而且在你会用并熟练地情况下,还会很好用,用起来很舒服,逻辑也很合理。下面就是一段代码,当然也是借鉴了别人的东西,自己修改了一下,在MainState中添加了一个Transition做了测试,因为此前我还不知道一个状态如何包含多个Transition,呵呵,原来是用mpl::list来做。至于这个状态机的入门教程,网上随处可见的三部曲:《boost 状态机入门教程》说得很清楚。
1 #include <iostream>
2 #include <ctime>
3
4 #include <boost/statechart/transition.hpp>
5 #include <boost/statechart/event.hpp>
6 #include <boost/statechart/state_machine.hpp>
7 #include <boost/statechart/simple_state.hpp>
8
9 namespace sc = boost::statechart;
10
11
12
13 class EvtStartStop : public sc::event<EvtStartStop>{};
14 class EvtReset : public sc::event<EvtReset>{};
15 class EvtGo : public sc::event<EvtGo>{};
16
17
18 class MainState;
19 class StopState;
20 class RunState;
21 class TwoState;
22
23 class Machine : public sc::state_machine<Machine, MainState>
24 {};
25
26
27
28
29
30
31 class MainState : public sc::simple_state<MainState, Machine, StopState>
32 {
33 public:
34 typedef sc::transition<EvtReset, MainState> reactReset;
35 typedef sc::transition<EvtGo, TwoState> reactGo;
36 typedef boost::mpl::list<reactReset, reactGo> reactions;
37
38 MainState(void){
39 std::cout<<"进入MainState"<<std::endl;
40 mTime = 0;
41 }
42
43 ~MainState(void){
44 std::cout<<"退出MainState"<<std::endl;
45 }
46
47 double mTime;
48 };
49
50
51 // 该状态属于无用状态,用于测试mpl::list的多transition用法
52 class TwoState : public sc::simple_state<TwoState, Machine>
53 {
54 public:
55 typedef sc::transition<EvtGo, MainState> reactions;
56
57 TwoState(void){
58 std::cout<<"进入TwoState"<<std::endl;
59 }
60
61 ~TwoState(void){
62 std::cout<<"退出TwoState"<<std::endl;
63 }
64 };
65
66
67 class StopState : public sc::simple_state<StopState, MainState>
68 {
69 public:
70 typedef sc::transition<EvtStartStop, RunState> reactions;
71 StopState(void){
72 std::cout<<"进入StopState"<<std::endl;
73 }
74
75 ~StopState(void){
76 std::cout<<"退出StopState"<<std::endl;
77 }
78 };
79
80 class RunState : public sc::simple_state<RunState, MainState>
81 {
82 public:
83 typedef sc::transition<EvtStartStop, StopState> reactions;
84 RunState(void){
85 std::cout<<"进入RunState"<<std::endl;
86 mStartTime = 0;
87 }
88
89 ~RunState(void){
90 std::cout<<"退出RunState"<<std::endl;
91 context<MainState>().mTime += std::difftime(std::time(0), mStartTime);
92 }
93
94 std::time_t mStartTime;
95 };
96
97
98 int _tmain(int argc, _TCHAR* argv[])
99 {
100 Machine mc;
101 mc.initiate();
102
103 mc.process_event(EvtStartStop());
104 mc.process_event(EvtStartStop());
105 mc.process_event(EvtReset());
106 mc.process_event(EvtGo());
107 mc.process_event(EvtGo());
108
109 return 0;
110 }
1.2 增加动作
此时我们将只用一种动作:transitions,我们在下面的代码中插入了黑体的部分。
1#include <boost/statechart/transition.hpp>
2
3//
4
5struct Stopped;
6struct Active : sc::simple_state< Active, StopWatch, Stopped >
7{
8 typedef sc::transition< EvReset, Active > reactions;
9};
10
11struct Running : sc::simple_state< Running, Active >
12{
13 typedef sc::transition< EvStartStop, Stopped > reactions;
14};
15
16struct Stopped : sc::simple_state< Stopped, Active >
17{
18 typedef sc::transition< EvStartStop, Running > reactions;
19};
20
21//一个状态可以定义任意数量的动作。这就是为什么当多于一个时,
22//我们不得不将它们放到一个mpl::list<> 里。
23
24int main()
25{
26 StopWatch myWatch;
27 myWatch.initiate();
28 myWatch.process_event( EvStartStop() );
29 myWatch.process_event( EvStartStop() );
30 myWatch.process_event( EvStartStop() );
31 myWatch.process_event( EvReset() );
32 return 0;
33}
34
现在我们有了所有的状态,并在适当的位置增加了所有的迁移动作,同时我们也向StopWatch发送了一些事件。这个状态机会尽职尽责的按我们的希望进行状态迁移,但依然现在还没有其它的动作。
1.3 State-local存储
下一步我们将让这个Stop watch真正的记录时间了。根据stop watch所处不同的状态,我们需要不同的变量。
l Stopped状态:需要一个保存逝去时间的变量。
l Running状态:需要一个保存逝去时间的变量,还需要一个保存上一次启动的时间点的变量。
无论状态机在什么状态下,我们都必须观察逝去时间这个变量。此外,当我们向状态机发送EvReSet事件时,这个变量应该被置为0。其它的变量只是状态机在Running状态时需要。无论何时我们进入Running状态时,它应该被置为系统时钟的当前时间。当我们退出Running状态时,我们仅仅从系统时钟的当前时间减去开始时间(进入时记录的时间),将结果加到逝去时间里就可以了。
1#include <ctime>
2
3//
4
5struct Stopped;
6struct Active : sc::simple_state< Active, StopWatch, Stopped >
7{
8 public :
9 typedef sc::transition< EvReset, Active > reactions;
10
11 Active() : elapsedTime_( 0.0 ) {}
12 double ElapsedTime() const { return elapsedTime_; }
13 double & ElapsedTime() { return elapsedTime_; }
14 private :
15 double elapsedTime_ ;
16};
17
18struct Running : sc::simple_state< Running, Active >
19{
20 public :
21 typedef sc::transition< EvStartStop, Stopped > reactions;
22
23 Running() : startTime_( std::time( 0 ) ) {}
24 ~Running()
25 {
26 // 与派生类可以访问它的基类相似,
27 //context<>() 用来获得一个状态的直接或间接的上下文的访问权。
28 // 这可以是直接或间接的外层状态或状态机本身
29 // (例如,像这样: context< StopWatch >()).
30 context< Active >().ElapsedTime() +=
31 std::difftime( std::time( 0 ), startTime_ );
32 }
33 private :
34 std:: time_t startTime_;
35};
这个状态机现在可以测量时间了,但是我们还不能看到结果。
在这里,State-local storage的优势还没有完成显现出来。在FAQ项目“State-local storage酷在哪里?”中,会通过与一个没有用State-local storage的Stop Watch的比较来说明。
1.4 在状态机外得到状态信息
为了取得测量的时间,我们需要一个从状态机外得到状态信息的机制。按我们现在的状态机设计,可以有两种方法。为简单起见,我们在这里用一个低效的方式:state_cast<>()(在StopWatch2.cpp中我们会用一个稍复杂一点的替代方法)(译者注:在StopWatch2.cpp中是向状态机发送一个取得逝去时间的事件,从事件成员量中将逝去时间带回来 ),从字面意思就可以看出,它在语义上与dynamic_cast有点相似。例如,当我们调用myWatch.state_cast<const Stpped&>()时,当状态机在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;
}
为了确实看到被测量的时间,你应该想办法在main()中单步执行。StopWatch例子将这个程序扩展为一个交互式的终端程序了。
1 基础主题:秒表
下面我们要为一个机械秒表建模一个状态机。这样一个秒表通常会有两个按钮。
* Start/Stop
* Reset
同时有两种状态:
* Stoped: 表针停留在上次停止时的位置:
o 按下Reset按钮,表针回退到0的位置。秒表保持在Stoped状态不变。
o 按下Start/Stop按钮,秒表转到Running状态。
* Running: 表针在移动,并持续显示过去的时间:
o 按下Reset按钮,表针回退到0的位置,秒表转到停止状态。
o 按下Start/Stop按钮,转到Stoped状态。
下面是其UML图:
1.1 定义状态和事件
两个按钮可以建模为两个事件。进而,定义出必要的状态和初始状态。我们从下面的代码开始,以前的代码片段会陆续加入其中:
#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个参数:
// - 第3个参数指定内部的初始状态,如果有一个这样的状态的话。
// 在这里,Active有一个内部状态(Stoped), 所以将这个内部
// 初始状态传给它的基类。
// - 第4个参数指定是否保留和保留什么类型历史
// 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;
}
这个代码已经可以编译了,但不会发生任何可察觉的事件。
介绍
Boost状态机库一个应用程序框架,你可以用它将UML状态图快速的转换为可执行的c++代码,而不需要任何的代码生成器。它支持几乎所有的UML特征,可以直接了当的转换,并且转换后的c++代码就像对状态机进行一次文本描述一样具体可读性。
如何阅读这个教程
这个教程是以线性阅读的方式进行的章节设计。如果你是第一次看这个教程的话,你可以从头开始读,到你觉得了解的东西对你手头的任务来说已经足够时就停止。具体可以这样:
* 如果你的任务是要实现一个小的、简单的,并且有很少几个状态的状态机,那么下面的“初级主题:秒表”里所讲的就差不多够你用的了。
* 如果你要做一个有很多状态的大型状态机,你可以看一下“中级主题:数码相机”,那里的讲解可能对你有帮助。
* 最后,如果你是一个要创建异常复杂状态机的用户,或者是一个想要评估一个Boost状态机的设计师的话,你就要看一下“高级主题”部分。并且,我还强烈建议你看一下Rationle里的Limitions部分。
Hello World!
我们将要从一个最简单程序开始我们的第一步,状态图如下:
对于这个状态图,我们的实现代码如下:
1#include <boost/statechart/state_machine.hpp>
2#include <boost/statechart/simple_state.hpp>
3#include <iostream>
4
5namespace sc = boost::statechart;
6
7// 为了避免写public,下面声明的类型全部为struct。
8// 如果你不在乎的话可以把它们都改成class。
9
10// 我们需要先声明一下初始状态,这是因为我们要在定义状态机时使用它
11// 但又不得不在状态机这后定义它。
12
13struct Greeting;
14
15// Boost.Statechart大量应用模板模式。
16// 派生类必须将自己做为基类模板的第一个参数。
17//
18// 状态机必须要知道当其初始化后进行的第一个状态。
19// 这就是为什么Greeting要做为每二个模板参数。
20// (译者注:也就是说Greeting状态是Machine状态机初始化后进入的第一个状态)
21struct Machine : sc::state_machine< Machine, Greeting > {};
22
23// 对于每一个状态,我们需要为其指明:它属于哪一个状态机,它位于状态图的哪个位置。
24// 我们用simple_state<>的上下文参数就可以完成这些指定了。
25// 对于我们目前的这个简单的状态机来说,上下文就是状态机(Machine)
26// 所以,Machine必须要做为simple_state的第二个模块参数。
27// (关于上下文参数的详细解释在下一个例子中有)
28struct Greeting : sc::simple_state< Greeting, Machine >
29{
30 // 一旦状态机进行一个状态的时候,它就要创建一个相应状态类的对象(类实例)
31 // 只要状态机保持在这个状态下,这个对象就会一直存在。
32 // 最后,当状态机离开这个状态时,对象被销毁。
33 // 所以,一个状态的进入动作就是这个状态类的构造器,而它的退出动作则是它的析构类。
Greeting() { std::cout << "Hello World!\n" ; } // 进入
34 ~Greeting() { std::cout << "Bye Bye World!\n" ; } // 退出
35};
36
37int main()
38{
39 Machine myMachine;
40 // 构造完状态机后,它并未开始运行。我们要通过调用它的initiate()来启动它。
41 // 同时,它也将触发它的初始状态(Greeting)的构造。
42 myMachine.initiate();
43 // 当我们离开main()函数时,myMachine将被销毁,这将导致它销毁它内部的所有活动的状态类。
44 // (译者注:为什么会说所有?这是因为一个状态机可以同时保持在多个状态中,可以参考“高级主题”部分)
45 return 0;
46}
这个程序会显示“Hello World!”和“ Bye Bye World! ”,然后退出。

