跳到主要内容

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?”试图通过将这个秒表与不使用状态本地存储的秒表进行比较来更详细地解释它们。

从状态机获取状态信息

要获取测量的时间,我们需要一种机制来从状态机中获取状态信息。 使用我们当前的状态机设计,有两种方法可以做到这一点。 为了简单起见,我们使用效率较低的一种:state_cast<>()StopWatch2.cpp显示了稍微复杂一些的替代方法)。 顾名思义,语义与dynamic_cast非常相似。 例如,当我们调用myWatch.state_cast<const Stopped&>()并且状态机当前处于Stopped状态时,我们将获得对Stopped状态的引用。 否则将引发std::bad_cast。 我们可以使用此函数来实现StopWatch成员函数,该函数返回经过的时间。 但是,我们没有询问状态机处于哪个状态,然后针对经过的时间切换到不同的计算,而是将计算置于“已停止”和“正在运行”状态,并使用一个接口来检索经过的时间:

#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()中的语句。秒表示例将此程序扩展到交互式控制台应用程序。

中级主题:数码相机

到目前为止,一切都很好。但是,上面介绍的方法有一些局限性:

  • 可伸缩性差:编译器到达state_machine::initiate()被调用的位置时,就会发生许多模板实例化,这些实例化仅在知道状态机每个State的完整声明时才能成功。也就是说,状态机的整个布局必须在一个translation unit中实现(Actions可以单独编译,但这在这里并不重要)。对于更大(更真实)的状态机,这导致以下限制:
    • 在某些时候,编译器达到其内部模板实例化限制并放弃。即使对于中等大小的机器,也会发生这种情况。例如,在调试模式下,一个流行的编译器拒绝为3位以上的任何内容编译BitMachine示例的早期版本。这意味着编译器在8个States,24个Transitions 和16个States,64个Transitions之间达到了极限
    • 多个程序员几乎无法同时在同一个状态机上工作,因为每次布局更改都会不可避免地导致整个状态机的重新编译
  • 每个Event最多一个Reaction:根据UML,一个State可以由同一Event触发多个Reactions。当所有Reactions都有互斥的防护装置时,这才有意义。我们上面使用的接口最多只能为每个事件提供一个不受保护的Reaction。此外,不直接支持UML概念的junction point和choice point。

所有这些限制都可以通过custom reactions来克服。警告:很容易滥用custom reactions导致调用未定义行为。在使用它们之前,请先阅读文档!

将状态机分布在多个转换单元上

假设您的公司想开发数码相机。 相机具有以下控件:

  • 快门按钮,可以半按和全按。 相关事件是EvShutterHalfEvShutterFullEvShutterReleased
  • Config按钮,由EvConfig事件表示
  • 此处不涉及的许多其他按钮

相机的一个使用案例表明,摄影师可以在配置模式下的任何位置半按快门,相机将立即进入拍摄模式。 下面的状态图是实现此行为的一种方法:

ConfiguringShooting State将包含许多嵌套State,而Idle状态则相对简单。 因此,决定组建两个团队。 一个将实现拍摄模式,而另一个将实现配置模式。 这两个团队已经就shooting团队将用于检索配置设置的接口达成了一致。 我们希望确保两个团队可以在最小的干扰下工作。 因此,我们将这两个State放在各自的转换单元中,以便在Configuring State 下更改机器布局不会导致重新编译Shooting状态的内部工作原理,反之亦然。

与前面的示例不同,此处提供的摘录通常概述了实现相同效果的不同选项。 这就是为什么代码通常不等于Camera示例代码的原因。 在这种情况下,注释会标记零件。

// Camera.hpp
#ifndef CAMERA_HPP_INCLUDED
#define CAMERA_HPP_INCLUDED

#include <boost/statechart/event.hpp>
#include <boost/statechart/state_machine.hpp>
#include <boost/statechart/simple_state.hpp>
#include <boost/statechart/custom_reaction.hpp>

namespace sc = boost::statechart;

struct EvShutterHalf : sc::event< EvShutterHalf > {};
struct EvShutterFull : sc::event< EvShutterFull > {};
struct EvShutterRelease : sc::event< EvShutterRelease > {};
struct EvConfig : sc::event< EvConfig > {};

struct NotShooting;
struct Camera : sc::state_machine< Camera, NotShooting > {
bool IsMemoryAvailable() const { return true; }
bool IsBatteryLow() const { return false; }
};

struct Idle;
struct NotShooting : sc::simple_state<
NotShooting, Camera, Idle > {
// With a custom reaction we only specify that we might do
// something with a particular event, but the actual reaction
// is defined in the react member function, which can be
// implemented in the .cpp file.
typedef sc::custom_reaction< EvShutterHalf > reactions;
// ...
sc::result react( const EvShutterHalf & );
};

struct Idle : sc::simple_state< Idle, NotShooting > {
typedef sc::custom_reaction< EvConfig > reactions;
// ...
sc::result react( const EvConfig & );
};
#endif
#include "Camera.hpp"

// The following includes are only made here but not in
// Camera.hpp
// The Shooting and Configuring states can themselves apply the
// same pattern to hide their inner implementation, which
// ensures that the two teams working on the Camera state
// machine will never need to disturb each other.
#include "Configuring.hpp"
#include "Shooting.hpp"

// ...

// not part of the Camera example
sc::result NotShooting::react( const EvShutterHalf & ) {
return transit< Shooting >();
}

sc::result Idle::react( const EvConfig & ) {
return transit< Configuring >();
}

注意:任何对simple_state<>::transit<>()simple_state<>::terminate()的调用(请参阅Reference)都将不可避免地破坏State Object(类似于删除此对象)! 也就是说,在任何这些调用之后执行的代码都可能会调用未定义的行为! 这就是为什么只应在return语句中调用这些函数的原因。

推迟事件

Shooting状态的内部工作原理如下所示:

当用户半按快门时,将进入Shooting及其内部初始状态Focusing。 在Focusing Entry Action 中,相机通知对焦电路将被摄物体对焦。 然后,对焦电路会相应地移动镜头,并在事件完成后立即发送EvInFocus Event。 当然,当镜头仍在运动时,用户可以完全按下快门。 如果没有任何预防措施,则由于Focusing State未定义对此事件的Reaction,因此将导致EvShutterFull Event丢失而不被处理。 结果,在相机完成对焦后,用户将不得不再次完全按下快门。 为避免这种情况,EvShutterFull事件在Focusing State内被延迟。 这意味着所有此类事件都存储在单独的队列中,当退出Focusing State时,该队列将被清空到主队列中。

struct Focusing : sc::state< Focusing, Shooting > {
typedef mpl::list<
sc::custom_reaction< EvInFocus >,
sc::deferral< EvShutterFull >
> reactions;

Focusing( my_context ctx );
sc::result react( const EvInFocus & );
};

守卫条件

Focused State两个Transitions都由同一事件触发,但是它们具有互斥的Guard。 这是一个适当的自定义Reaction :

// not part of the Camera example
sc::result Focused::react( const EvShutterFull & ) {
if ( context< Camera >().IsMemoryAvailable() )
{
return transit< Storing >();
}
else
{
// The following is actually a mixture between an in-state
// reaction and a transition. See later on how to implement
// proper transition actions.
std::cout << "Cache memory full. Please wait...\n";
return transit< Focused >();
}
}

当然,也可以直接在State声明中实现自定义Reactions,通常最好使用自定义Reactions,以便于浏览。

接下来,我们将使用Guard来防止Transition,并在电池电量不足时让外部状态对事件做出反应:

// Camera.cpp
// ...
sc::result NotShooting::react( const EvShutterHalf & )
{
if ( context< Camera >().IsBatteryLow() )
{
// We cannot react to the event ourselves, so we forward it
// to our outer state (this is also the default if a state
// defines no reaction for a given event).
return forward_event();
}
else
{
return transit< Shooting >();
}
}
// ...

状态内Reactions

Focused State的self-transition也可以实现为in-state reaction,只要Focused没有任何entry actions和exit actions,其效果相同。

// Shooting.cpp:
// ...
sc::result Focused::react( const EvShutterFull & ) {
if ( context< Camera >().IsMemoryAvailable() ) {
return transit< Storing >();
} else {
std::cout << "Cache memory full. Please wait...\n";
// Indicate that the event can be discarded. So, the
// dispatch algorithm will stop looking for a reaction
// and the machine remains in the Focused state.
return discard_event();
}
}
// ...

由于in-state reaction是guarded,因此我们需要在此处使用custom_reaction<>。 对于unguarded的in-state reactions,应使用in_state_reaction<>以获得更好的代码可读性。

Transition Actions

每次Transition都会以下列顺序执行操作:

  • 从最内层的active state开始,所有exit actions直到但不包括innermost common context
  • Transition Actions(如果有)
  • 从innermost common context开始,所有entry actions一直到目标状态,然后是初始状态的entry action

例子:

这里的顺序如下:~D()~C()~B()~A()t()X()Y()Z()。 因此,在InnermostCommonOuter状态的上下文中执行Transition Action t(),因为已经退出源状态(销毁)而目标状态尚未被输入(构建)。

使用Boost.Statechart,Transition Action可以是任何common outer context的成员。 也就是说,可以通过以下方式实现Focusing和Focused之间的transition:

// Shooting.hpp:

// ...
struct Focusing;
struct Shooting : sc::simple_state< Shooting, Camera, Focusing > {
typedef sc::transition<EvShutterRelease, NotShooting > reactions;
// ...
void DisplayFocused( const EvInFocus & );
};

// ...

// not part of the Camera example
struct Focusing : sc::simple_state< Focusing, Shooting > {
typedef sc::transition< EvInFocus, Focused,Shooting, &Shooting::DisplayFocused > reactions;
};

或者,以下也是可能的(这里状态机本身是最外部的上下文):

// not part of the Camera example
struct Camera : sc::state_machine< Camera, NotShooting > {
void DisplayFocused( const EvInFocus & );
};
// not part of the Camera example
struct Focusing : sc::simple_state< Focusing, Shooting > {
typedef sc::transition< EvInFocus, Focused,Camera, &Camera::DisplayFocused > reactions;
};

当然,也可以从自定义反应中调用过渡动作:

// Shooting.cpp:

// ...
sc::result Focusing::react( const EvInFocus & evt )
{
// We have to manually forward evt
return transit< Focused >( &Shooting::DisplayFocused, evt );
}

进阶主题

指定一个状态的多个反应

通常,一个state必须定义一个以上事件的反应。 在这种情况下,必须使用mpl::list<>,如下所示:

// ...

#include <boost/mpl/list.hpp>

namespace mpl = boost::mpl;

// ...

struct Playing : sc::simple_state< Playing, Mp3Player > {
typdef mpl::list<sc::custom_reaction< EvFastForward >,
sc::transition< EvStop, Stopped > > reactions;
/* ... */
};

发布事件

非平凡的状态机通常需要发布内部事件。 这是如何执行此操作的示例:

Pumping::~Pumping()  {
post_event( EvPumpingFinished() );
}

该事件被推入主队列。 当前reaction完成后,将立即处理队列中的事件。 可以从react函数内部,entry,exit和transition action中发布事件。 但是,从内部entry action进行post会更加复杂(例如,请参见Camera示例中Shooting.cpp中的Focusing::Focusing()):

struct Pumping : sc::state< Pumping, Purifier > {
Pumping( my_context ctx ) : my_base( ctx ) {
post_event( EvPumpingStarted() );
}
// ...
};

一旦状态的entry action需要联系outside world(这里是状态机中的事件队列),状态必须从state<>而不是simple_state<>派生,并且必须实现转发构造函数 上面概述的内容(除了构造函数之外,state<>提供的接口与simple_state<>相同)。 因此,每当entry action对以下函数进行一个或多个调用时,都必须执行此操作:

simple_state<>::post_event()
simple_state<>::clear_shallow_history<>()
simple_state<>::clear_deep_history<>()
simple_state<>::outermost_context()
simple_state<>::context<>()
simple_state<>::state_cast<>()
simple_state<>::state_downcast<>()
simple_state<>::state_begin()
simple_state<>::state_end()

根据我的经验,这些功能仅在entry action中很少需要,因此此解决方法不应过多地破坏用户代码。

历史

测试我们数码相机Beta版的摄影师说,他们真的很喜欢随时半按快门(即使在配置相机时)也可以立即为相机拍照。 但是,大多数人发现打开快门后相机始终进入空闲模式并不直观。 他们宁愿看到相机恢复到半按快门之前的状态。 这样,他们可以通过修改配置设置,半按然后完全按下快门以拍摄照片来轻松测试配置设置的影响。 最后,释放快门会将其带回到修改设置的屏幕。 为了实现此行为,我们将更改状态图,如下所示:

如前所述,Configuring 状态包含一个相当复杂且深度嵌套的内部状态机。 自然,我们想在配置中将以前的状态还原到最内层的状态,这就是为什么我们使用深度历史记录伪状态。 关联的代码如下所示:

// not part of the Camera example
struct NotShooting : sc::simple_state<
NotShooting, Camera, Idle, sc::has_deep_history >
{
// ...
};

// ...

struct Shooting : sc::simple_state< Shooting, Camera, Focusing >
{
typedef sc::transition<
EvShutterRelease, sc::deep_history< Idle > > reactions;

// ...
};

历史记录分为两个阶段:首先,当退出包含历史记录伪状态的状态时,必须保存有关先前处于活动状态的内部状态层次结构的信息。其次,当稍后转换到历史伪状态时,必须检索保存的状态层次结构信息并输入适当的状态。前者通过将has_shallow_history,has_deep_history或has_full_history(将浅层和深层历史结合)作为最后一个参数传递给simple_state和state类模板来表示。后者通过指定shallow_history<>deep_history<>作transion目标,或者如我们将在瞬间看到的那样,指定为内部初始状态来表示。因为可能在进行向历史记录的转换之前从未输入过包含历史记录伪状态的状态,所以两个类模板都需要一个参数,该参数指定在这种情况下要输入的默认状态。

在编译时检查使用历史记录所需的冗余是否一致。也就是说,如果我们忘记将has_deep_history传递给NotShooting的基础,那么状态机就不会编译。

一些Beta测试人员提出的另一项变更请求说,他们希望看到相机恢复到打开之前的状态,然后再重新打开。这是实现:

// ...

// not part of the Camera example
struct NotShooting : sc::simple_state< NotShooting, Camera,
mpl::list< sc::deep_history< Idle > >,
sc::has_deep_history >
{
// ...
};

// ...

不幸的是,由于一些与模板相关的实现细节,因此带来了一些不便。 当内部初始状态是类模板实例化时,尽管只有一个内部初始状态,我们总是必须将其放入mpl::list<>中。 此外,当前的深度历史实现具有一些局限性。

正交态

要实现此状态图,您只需指定多个内部初始状态(请参见Keyboard 示例):

struct Active;
struct Keyboard : sc::state_machine< Keyboard, Active > {};

struct NumLockOff;
struct CapsLockOff;
struct ScrollLockOff;
struct Active: sc::simple_state< Active, Keyboard,
mpl::list< NumLockOff, CapsLockOff, ScrollLockOff > > {};

Active的内部状态必须声明它们属于哪个正交区域:

struct EvNumLockPressed : sc::event< EvNumLockPressed > {};
struct EvCapsLockPressed : sc::event< EvCapsLockPressed > {};
struct EvScrollLockPressed :
sc::event< EvScrollLockPressed > {};

struct NumLockOn : sc::simple_state<
NumLockOn, Active::orthogonal< 0 > >
{
typedef sc::transition<
EvNumLockPressed, NumLockOff > reactions;
};

struct NumLockOff : sc::simple_state<
NumLockOff, Active::orthogonal< 0 > >
{
typedef sc::transition<
EvNumLockPressed, NumLockOn > reactions;
};

struct CapsLockOn : sc::simple_state<
CapsLockOn, Active::orthogonal< 1 > >
{
typedef sc::transition<
EvCapsLockPressed, CapsLockOff > reactions;
};

struct CapsLockOff : sc::simple_state<
CapsLockOff, Active::orthogonal< 1 > >
{
typedef sc::transition<
EvCapsLockPressed, CapsLockOn > reactions;
};

struct ScrollLockOn : sc::simple_state<
ScrollLockOn, Active::orthogonal< 2 > >
{
typedef sc::transition<
EvScrollLockPressed, ScrollLockOff > reactions;
};

struct ScrollLockOff : sc::simple_state<
ScrollLockOff, Active::orthogonal< 2 > >
{
typedef sc::transition<
EvScrollLockPressed, ScrollLockOn > reactions;
};

orthogonal<0>是默认设置,因此NumLockOnNumLockOff可以传递Active而不是Active::orthogonal<0>来指定其上下文。 传递给orthogonal 成员模板的数字必须与外部状态中的列表位置相对应。 此外,transition 的源状态的正交位置必须与目标状态的正交位置相对应。 违反这些规则将导致编译时错误。 例子:

// Example 1: does not compile because Active specifies
// only 3 orthogonal regions
struct WhateverLockOn: sc::simple_state<
WhateverLockOn, Active::orthogonal< 3 > > {};

// Example 2: does not compile because Active specifies
// that NumLockOff is part of the "0th" orthogonal region
struct NumLockOff : sc::simple_state<
NumLockOff, Active::orthogonal< 1 > > {};

// Example 3: does not compile because a transition between
// different orthogonal regions is not permitted
struct CapsLockOn : sc::simple_state<
CapsLockOn, Active::orthogonal< 1 > >
{
typedef sc::transition<
EvCapsLockPressed, CapsLockOff > reactions;
};

struct CapsLockOff : sc::simple_state<
CapsLockOff, Active::orthogonal< 2 > >
{
typedef sc::transition<
EvCapsLockPressed, CapsLockOn > reactions;
};

状态查询

状态机中的reactions通常取决于一个或多个正交区域中的活动状态。 这是因为正交区域不是完全正交的,或者仅当内部正交区域处于特定状态时才可能在外部状态下发生某种reaction。 为此,在状态中还可以使用在从状态机中获取状态信息下介绍的state_cast <>函数。

作为一个牵强附会的示例,让我们假设键盘也接受EvRequestShutdown事件,只有当所有锁定键都处于off状态时,该事件的接收才使键盘终止。 然后,我们将修改键盘状态机,如下所示:

struct EvRequestShutdown : sc::event< EvRequestShutdown > {};

struct NumLockOff;
struct CapsLockOff;
struct ScrollLockOff;
struct Active: sc::simple_state< Active, Keyboard,
mpl::list< NumLockOff, CapsLockOff, ScrollLockOff > > {
typedef sc::custom_reaction< EvRequestShutdown > reactions;

sc::result react( const EvRequestShutdown & ) {
if ( ( state_downcast< const NumLockOff * >() != 0 ) &&
( state_downcast< const CapsLockOff * >() != 0 ) &&
( state_downcast< const ScrollLockOff * >() != 0 ) ) {
return terminate();
} else {
return discard_event();
}
}
};

传递指针类型而不是引用类型会导致强制转换失败时返回0指针,而不是引发std::bad_cast。 另请注意,使用state_downcast<>()代替state_cast<>()。 与boost::polymorphic_downcast<>()dynamic_cast之间的区别类似,state_downcast<>()state_cast<>()的一个快得多的变体,并且只能在传递的类型是最底层(most-derived)类型时使用。 state_cast<>()仅在要查询其他基准时才应使用。

自定义状态查询

通常需要精确找出状态机当前处于哪个状态。在某种程度上,使用state_cast<>()state_downcast<>()已经可以实现,但是它们的效用受到限制,因为它们都只返回yes/no来回答“您是否处于X State?”的问题。当您向state_cast<>()传递附加的基类而不是状态类时,可能会提出更复杂的问题,但这涉及更多的工作(所有状态都需要从附加的基类派生并实现),速度很慢(在hood state_cast<>()使用dynamic_cast),强制项目在C ++ RTTI打开的情况下进行编译,并对状态entry/exit速度产生负面影响。

尤其对于调试而言,能够询问“您处于哪种状态?”将更加有用。为此,可以使用state_machine <> :: state_begin()state_machine <> :: state_end()遍历所有活动的最里面的状态。解引用返回的迭代器将返回对const state_machine<>::state_base_type的引用,const是所有状态的公共基数。因此,我们可以按以下方式打印当前处于活动状态的配置(有关完整代码,请参见Keyboard示例):

void DisplayStateConfiguration( const Keyboard & kbd )
{
char region = 'a';

for (
Keyboard::state_iterator pLeafState = kbd.state_begin();
pLeafState != kbd.state_end(); ++pLeafState )
{
std::cout << "Orthogonal region " << region << ": ";
// The following use of typeid assumes that
// BOOST_STATECHART_USE_NATIVE_RTTI is defined
std::cout << typeid( *pLeafState ).name() << "\n";
++region;
}
}

如有必要,可以使用state_machine<>::state_base_type::outer_state_ptr()访问外部状态,该状态返回指向const state_machine<>::state_base_type的指针。 在最外层状态被调用时,此函数仅返回0。

状态类型信息

异常处理

子机和参数化状态

异步状态机

为什么需要异步状态机

顾名思义,同步状态机同步处理每个事件。此行为由state_machine类模板实现,该模板的process_event函数仅在执行所有reactions(包括action可能已发布的内部事件引起的reactions)之后返回。此函数严格不可重入(就像所有其他成员函数一样,因此state_machine<>也不是线程安全的)。这使得两个state_machine<>子类型对象即使在单线程程序中也很难通过事件以双向方式正确进行通信。例如,状态机A正在处理外部事件。在动作内部,它决定将新事件发送到状态机B(通过调用B::process_event())。然后,它wait B通过类似于boost::function <>的回调发送回答案,该回调引用A::process_event()并作为事件的数据成员传递。但是,当A在waitB发送回事件时,A::process_event()尚未从处理外部事件中返回,并且B通过回调进行响应后,A::process_event()便立即返回不可避免地重新进入。所有这些实际上都在单个线程中发生,这就是为什么wait用引号引起来。

怎么运行的

asynchronous_state_machine类模板不具有state_machine类模板具有的成员函数。而且,asynchronous_state_machine<>子类型对象甚至无法直接创建或销毁。相反,所有这些操作必须通过与每个异步状态机关联的Scheduler对象执行。所有这些Scheduler成员函数仅将适当的项目推送到调度程序的队列中,然后立即返回。专用线程稍后会将项目弹出队列以进行处理。

应用程序通常会首先创建一个fifo_scheduler<>对象,然后调用fifo_scheduler<>::create_processor<>()fifo_scheduler<>::initiate_processor()安排一个或多个asynchronous_state_machine<>子类型对象的创建和初始化。最后,可以直接调用fifo_scheduler<>::operator()()以使状态机在当前线程中运行,或者将引用operator()()boost::function<>对象传递给新对象。 boost::thread。另外,后者也可以在构造fifo_scheduler<>对象之后立即完成。在以下代码中,我们在一个新的boost :: thread中运行一个状态机,在主线程中运行另一个状态机(有关完整的源代码,请参见PingPong示例):

struct Waiting;
struct Player :
sc::asynchronous_state_machine< Player, Waiting >
{
// ...
};

// ...

int main()
{
// Create two schedulers that will wait for new events
// when their event queue runs empty
sc::fifo_scheduler<> scheduler1( true );
sc::fifo_scheduler<> scheduler2( true );

// Each player is serviced by its own scheduler
sc::fifo_scheduler<>::processor_handle player1 =
scheduler1.create_processor< Player >( /* ... */ );
scheduler1.initiate_processor( player1 );
sc::fifo_scheduler<>::processor_handle player2 =
scheduler2.create_processor< Player >( /* ... */ );
scheduler2.initiate_processor( player2 );

// the initial event that will start the game
boost::intrusive_ptr< BallReturned > pInitialBall =
new BallReturned();

// ...

scheduler2.queue_event( player2, pInitialBall );

// ...

// Up until here no state machines exist yet. They
// will be created when operator()() is called

// Run first scheduler in a new thread
boost::thread otherThread( boost::bind(
&sc::fifo_scheduler<>::operator(), &scheduler1, 0 ) );
scheduler2(); // Run second scheduler in this thread
otherThread.join();

return 0;
}

我们也可以使用两个boost::threads

int main()
{
// ...

boost::thread thread1( boost::bind(
&sc::fifo_scheduler<>::operator(), &scheduler1, 0 ) );
boost::thread thread2( boost::bind(
&sc::fifo_scheduler<>::operator(), &scheduler2, 0 ) );

// do something else ...

thread1.join();
thread2.join();

return 0;
}

或者,在同一线程中运行两个状态机:

int main()
{
sc::fifo_scheduler<> scheduler1( true );

sc::fifo_scheduler<>::processor_handle player1 =
scheduler1.create_processor< Player >( /* ... */ );
sc::fifo_scheduler<>::processor_handle player2 =
scheduler1.create_processor< Player >( /* ... */ );

// ...

scheduler1();

return 0;
}

在以上所有示例中,fifo_scheduler<>::operator()()等待空的事件队列,并且仅在调用fifo_scheduler<>::terminate()之后返回。Player状态机在终止之前立即在其调度程序对象上调用此函数。