Boost.Statechart
简介
Boost Statechart库是一个允许您快速转换UML状态图框架转换成可执行的C++代码,不需要使用代码生成器。
感谢几乎所有人的支持UML特性的转换是直接的,生成的C++代码是近似的状态图的无冗余文本描述。
如何阅读本教程
本教程被设计成线性阅读。第一次使用时,用户应该从头开始阅读,一旦他们对手头的任务有了足够的了解,就停止。具体地说:
-
通过使用“基本主题:秒表”中介绍的功能,可以合理地实现只有少数状态的小型和简单状态机。
-
对于状态数不超过12个的大型机器,在medium下描述的特性主题:数码相机通常很有用
-
最后,用户希望创建更复杂的机器和项目架构师评估Boost.Statechart还应该在最后阅读Advanced topics部分。此外,阅读强烈建议在基本原理中加入限制部分
Hello World!
我们将使用最简单的程序来完成第一步。状态图……
是用以下代码来实现的:
#include <boost/statechart/state_machine.hpp>
#include <boost/statechart/simple_state.hpp>
#include <iostream>
namespace sc = boost::statechart;
/*我们将所有类型声明为struct,只是为了避免必须键入public。如果你不介意这样做,你也可以这样做使用class。我们需要前向声明初始状态,因为它只能在状态机已经定义的位置定义。*/
struct Greeting;
/*Boost.Statechart大量使用模板递归模式。派生类必须始终作为所有基类模板的第一个参数。必须告知状态机在启动时必须进入哪个状态。这就是为什么Greeting作为第二个模板参数传递的原因。*/
struct Machine : sc::state_machine< Machine, Greeting > {};
/*对于每个状态,我们需要定义它属于哪个状态机,并且位于状态图中的什么位置。它们都是使用传递给的上下文参数指定simple_state < >。对于我们这里的平面状态机,上下文总是状态机。因此,必须将Machine作为第二个模板参数传递给Greeting的base(下一个示例将更详细地解释上下文参数)。*/
struct Greeting : sc::simple_state< Greeting, Machine > {
/*无论何时状态机进入一个状态,它都会创建一个对应状态类的对象。然后,只要机器处于状态,对象就保持活动状态。最后,当状态机退出状态时,对象被销毁。因此,可以通过添加构造函数定义状态入口操作,通过添加析构函数定义状态退出操作。*/
Greeting() { std::cout << "Hello World!\n"; } // entry
~Greeting() { std::cout << "Bye Bye World!\n"; } // exit
};
int main() {
Machine myMachine;
/*状态机构造完成后还没有运转,首先调用initiate()。这将触发初始状态Greeting的构造*/
myMachine.initiate();
/*当我们离开main()时,myMachine被销毁,导致当前所有活动状态的销毁。*/
return 0;
}
该程序会在退出前先后打印Hello World!和Bye Bye World! 。
基本主题:秒表
接下来,我们将使用状态机为简单的机械秒表建模。 此类手表通常具有两个按钮:
- 开始/停止
- 复位
还有两个状态:
- 停止:指针停留在它们最后停止的位置:
- 按下复位按钮将指针移回0位置。指针保持停止状态
- 按下开始/停止按钮将导致转换到运行状态
- 运行:手表的指针在运动,并持续显示经过的时间
- 按下复位按钮将指针移回0位置,并导致切换到停止状态
- 按下开始/停止按钮将导致转换到停止状态
这里有一种在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;
// The simple_state class template accepts up to four parameters:
// - The third parameter specifies the inner initial state, if
// there is one. Here, only Active has inner states, which is
// why it needs to pass its inner initial state Stopped to its
// base
// - The fourth parameter specifies whether and what kind of
// history is kept
// Active是最外部的状态,因此需要传递其所属的状态机类
struct Active : sc::simple_state<
Active, StopWatch, Stopped > {};
// Stopped and Running both specify Active as their Context,
// which makes them nested inside Active
struct Running : sc::simple_state< Running, Active > {};
struct Stopped : sc::simple_state< Stopped, Active > {};
// Because the context of a state must be a complete type (i.e.
// not forward declared), a machine must be defined from
// "outside to inside". That is, we always start with the state
// machine, followed by outermost states, followed by the direct
// inner states of outermost states and so on. We can do so in a
// breadth-first or depth-first way or employ a mixture of the
// two.
int main()
{
StopWatch myWatch;
myWatch.initiate();
return 0;
}
程序编译通过,但还没有执行任何可观察到的操作。
添加reactions
目前我们只使用一种reaction: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;
};
/*一个状态可以定义任意数目的反应。这就是为什么我们必须将它们放入mpl::list<>中,只要它们有多个*/
int main() {
StopWatch myWatch;
myWatch.initiate();
myWatch.process_event( EvStartStop() );
myWatch.process_event( EvStartStop() );
myWatch.process_event( EvStartStop() );
myWatch.process_event( EvReset() );
return 0;
}
现在,我们拥有所有状态和所有转换,并且还将许多事件发送到秒表。 机器尽职尽责地完成了我们所期望的转换,但尚未执行任何操作。
局部状态存储
接下来我们要让秒表真正测量时间。根据秒表所处的状态,我们需要不同的变量:
- Stopped:一个变量保存
elapsed time
。 - Running:一个变量保存
elapsed time
,另一个变量存储上一次启动手表的时间点。
我们观察到,无论状态机处于什么状态,都需要 elapsed time
变量。此外,当我们向状态机发送EvReset
事件时,应将此变量重置为0。 仅在状态机处于Running
状态时才需要另一个变量。 每当我们进入Running
状态时,应将其设置为系统时钟的当前时间。 退出时,我们只需从当前系统时钟时间中减去开始时间,并将结果添加到elapsed time
即可。
#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_;
};
// ...
机器现在可以测量时间,但我们仍无法从主程序中获取时间。
此时,状态本地存储的优势(这仍然是一个相对鲜为人知的特性)可能还不太明显。FAQ项目“What's so cool about state-local storage?”试图通过将这个秒表与不使用状态本地存储的秒表进行比较来更详细地解释它们。