Boost状态图库
简介
Boost 状态图库是一个无需代码生成器就可以快速转换 UML 状态图表伪可执行的 C++ 代码框架。由于几乎支持所有 UML的特性直接转换,故所产生的C++代码十分接近于状态表的原文文字描述。
如何阅读此教程
该教程内容设计为逐步深入。读者可以从合适位置开始读,直到对于解决你的任务为止。特别地:
- 首先,仅仅有少数几个状态的简单状态机能够通过以下描述合理地实现,基本主题:秒表
- 其次,达到12个状态的大型状态机请参照以下文章:进阶主题:数码相机应该是很有帮助的。
- 最后,用户想要创建更加复杂的状态机,请参照:高级主题部分。另外,强烈建议阅读限制条件章节。
hello world
我们将用一个最简单的程序开始第一步,以下为状态表:
该状态图的实现代码如下:
#include <boost/statechart/state_machine.hpp>
#include <boost/statechart/simple_state.hpp>
#include <iostream>
namespace sc = boost::statechart;
// 此处定义为strcut 是为了避免所有成员都要添加 public关键字,如果你不介意,也可以使用class。
// 我们需要提前申明初始化状态,因为其必须在定义状态机的地方定义。
struct Greeting;
// Boost.Statechart 重度使用模板。子类必须总是将该初始化状态作为第一参数传递给基类
// 当状态机进行初始化时,必须被通知进入哪个状态这就是为什么 Greeting 作为第二个参数传递的原因。
struct Machine : sc::state_machine< Machine, Greeting > {};
//对于每个状态都必须指定其所属状态机以及其在状态表中的位置,
//两者是通过simple_state<>的参数来指定。
//因为我们拥有简单的状态机,故其上下文也是状态机。
//相应的,Machine 被当作上下文作为第二参数传递(上下文参数在下文将会进一步说明)。
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;
// myMachine 在构造函数调用后并没有运行,我们需要通过调用 initiate() 运行该状态机,
//这将触发 初始状态 Greeting 的构造
myMachine.initiate();
// 当离开 main()时,myMachine 的析构将导致当前激活状态被析构
return 0;
}
执行此程序输出 Hello World! 和 Bye Bye World!。
基本主题-秒表
接下来我们将创建一个简单的状态机---秒表。此秒表有两个按钮:
- 开始/停止
- 复位
以及两种状态:
- 停止:指针位于其最后停止的位置,此时秒表处于停止状态。按复位按钮可以复位指针到最初位置,按开始/停止按钮秒表将转变为运行状态。
- 运行:指针处于移动中,连续显示时间地流逝。按复位按钮将导致指针移动到最初位置,秒表状态转变为停止状态,按开始/停止按钮秒表状态将转变为停止状态。
如下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()中的语句。你也可以将此秒表程序扩展为一个交互式的控制台应用。