C++ Boost MetaStateMachine定义状态机超详细讲解

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

C++ Boost MetaStateMachine定义状态机超详细讲解

无水先生   2022-12-07 我要评论

一、说明

Boost.MetaStateMachine 用于定义状态机。状态机通过对象的状态来描述对象。它们描述了存在哪些状态以及状态之间可能存在哪些转换。

Boost.MetaStateMachine 提供了三种不同的方式来定义状态机。创建状态机所需编写的代码取决于前端。

如果使用基本前端或函数前端,则可以用常规方式定义状态机:创建类,从 Boost.MetaStateMachine 提供的其他类派生它们,定义所需的成员变量,并编写所需的 C++自己编码。基本前端和函数前端的根本区别在于,基本前端需要函数指针,而函数前端让你使用函数对象。

第三个前端称为 eUML,它基于特定领域的语言。该前端可以通过重用 UML 状态机的定义来定义状态机。熟悉 UML 的开发人员可以将 UML 行为图中的定义复制到 C++ 代码。您不需要将 UML 定义转换为 C++ 代码。

eUML 基于您必须与此前端一起使用的一组宏。宏的优点是您不需要直接使用 Boost.MetaStateMachine 提供的许多类。您只需要知道要使用哪些宏。这意味着您不能忘记从类派生状态机,这可能发生在基本前端或函数前端。本章介绍使用 eUML 的 Boost.MetaStateMachine。

二、示例和代码

示例 68.1。使用 eUML 的简单状态机

#include <boost/msm/front/euml/euml.hpp>
#include <boost/msm/front/euml/state_grammar.hpp>
#include <boost/msm/back/state_machine.hpp>
#include <iostream>
namespace msm = boost::msm;
using namespace boost::msm::front::euml;
BOOST_MSM_EUML_STATE((), Off)
BOOST_MSM_EUML_STATE((), On)
BOOST_MSM_EUML_EVENT(press)
BOOST_MSM_EUML_TRANSITION_TABLE((
  Off + press == On,
  On + press == Off
), light_transition_table)
BOOST_MSM_EUML_DECLARE_STATE_MACHINE(
(light_transition_table, init_ << Off),
light_state_machine)
int main()
{
  msm::back::state_machine<light_state_machine> light;
  std::cout << *light.current_state() << '\n';
  light.process_event(press);
  std::cout << *light.current_state() << '\n';
  light.process_event(press);
  std::cout << *light.current_state() << '\n';
}

Example 68.1 

示例 68.1 使用了最简单的状态机:一盏灯恰好有两种状态。它可以打开或关闭。如果它是关闭的,它可以被打开。如果已打开,则可以将其关闭。可以从每个状态切换到每个其他状态。

示例 68.1 使用 eUML 前端来描述灯的状态机。 Boost.MetaStateMachine 没有主头文件。因此,必须将需要的头文件一一包含进来。 boost/msm/front/euml/euml.hpp 和 boost/msm/front/euml/state_grammar.hpp 提供对 eUML 宏的访问。 boost/msm/back/state_machine.hpp 需要将前端的状态机链接到后端的状态机。虽然前端提供了定义状态机的各种可能性,但状态机的实际实现是在后端找到的。由于 Boost.MetaStateMachine 仅包含一个后端,因此您无需选择实现。

Boost.MetaStateMachine 中的所有定义都在命名空间 boost::msm 中。不幸的是,许多 eUML 宏并没有明确引用这个命名空间中的类。他们使用命名空间 msm 或根本不使用命名空间。这就是为什么示例 68.1 为命名空间 boost::msm 创建了一个别名,并使 boost::msm::front::euml 中的定义可用于 using 指令。否则 eUML 宏会导致编译器错误。

要使用灯的状态机,首先要定义关和开的状态。状态是用宏 BOOST_MSM_EUML_STATE 定义的,它需要状态的名称作为它的第二个参数。第一个参数描述状态。稍后你会看到这些描述是什么样子的。示例 68.1 中定义的两个状态称为关闭和打开。

要在状态之间切换,需要事件。事件是用宏 BOOST_MSM_EUML_EVENT 定义的,它期望事件的名称作为其唯一参数。示例 68.1 定义了一个名为 press 的事件,它表示按下电灯开关的动作。由于同一事件会打开和关闭一盏灯,因此只定义了一个事件。

定义所需的状态和事件后,宏 BOOST_MSM_EUML_TRANSITION_TABLE 用于创建转换表。该表定义了状态之间的有效转换以及哪些事件触发了哪些状态转换。

BOOST_MSM_EUML_TRANSITION_TABLE 需要两个参数。第一个参数定义了转换表,第二个是转换表的名称。第一个参数的语法旨在使识别状态和事件如何相互关联变得容易。例如,Off + press == On 表示处于 Off 状态的机器通过按下事件切换到 On 状态。转换表定义的直观和不言自明的语法是 eUML 前端的优势之一。

创建转换表后,使用宏 BOOST_MSM_EUML_DECLARE_STATE_MACHINE 定义状态机。第二个参数也是更简单的一个:它设置状态机的名称。示例 68.1 中的状态机名为 light_state_machine。

BOOST_MSM_EUML_DECLARE_STATE_MACHINE 的第一个参数是一个元组。第一个值是转换表的名称。第二个值是一个使用 init_ 的表达式,它是 Boost.MetaStateMachine 提供的一个属性。稍后您将了解有关属性的更多信息。需要表达式 init_ << Off 将状态机的初始状态设置为 Off。

用 BOOST_MSM_EUML_DECLARE_STATE_MACHINE 定义的状态机 light_state_machine 是一个类。您使用此类从后端实例化状态机。在示例 68.1 中,这是通过将 light_state_machine 作为参数传递给类模板 boost::msm::back::state_machine 来完成的。这会创建一个名为 light 的状态机。

状态机提供一个成员函数 process_event() 来处理事件。如果您将事件传递给 process_event(),状态机会根据其转换表更改其状态。

为了更容易看到多次调用 process_event() 时示例 68.1 中发生的情况,调用了 current_state()。此成员函数应仅用于调试目的。它返回一个指向 int 的指针。每个状态都是一个 int 值,按照状态在 BOOST_MSM_EUML_TRANSITION_TABLE 中被访问的顺序分配。在示例 68.1 中,Off 被赋予值 0,而 On 被赋予值 1。该示例将 0、1 和 0 写入标准输出。按下灯开关两次,可以打开和关闭灯。

示例 68.2。具有状态、事件和动作的状态机

#include <boost/msm/front/euml/euml.hpp>
#include <boost/msm/front/euml/state_grammar.hpp>
#include <boost/msm/back/state_machine.hpp>
#include <iostream>
namespace msm = boost::msm;
using namespace boost::msm::front::euml;
BOOST_MSM_EUML_STATE((), Off)
BOOST_MSM_EUML_STATE((), On)
BOOST_MSM_EUML_EVENT(press)
BOOST_MSM_EUML_ACTION(switch_light)
{
  template <class Event, class Fsm>
  void operator()(const Event &ev, Fsm &fsm,
    BOOST_MSM_EUML_STATE_NAME(Off) &sourceState,
    BOOST_MSM_EUML_STATE_NAME(On) &targetState) const
  {
    std::cout << "Switching on\n";
  }
  template <class Event, class Fsm>
  void operator()(const Event &ev, Fsm &fsm,
    decltype(On) &sourceState,
    decltype(Off) &targetState) const
  {
    std::cout << "Switching off\n";
  }
};
BOOST_MSM_EUML_TRANSITION_TABLE((
  Off + press / switch_light == On,
  On + press / switch_light == Off
), light_transition_table)
BOOST_MSM_EUML_DECLARE_STATE_MACHINE(
(light_transition_table, init_ << Off),
light_state_machine)
int main()
{
  msm::back::state_machine<light_state_machine> light;
  light.process_event(press);
  light.process_event(press);
}

Example 68.2 

示例 68.2 通过一个动作扩展了灯的状态机。动作由触发状态转换的事件执行。因为动作是可选的,所以可以在没有它们的情况下定义状态机。

操作是使用 BOOST_MSM_EUML_ACTION 定义的。严格来说,定义了一个函数对象。您必须重载运算符 operator()。操作员必须接受四个参数。参数引用一个事件、一个状态机和两个状态。您可以自由定义模板或为所有参数使用具体类型。在示例 68.2 中,仅为最后两个参数设置具体类型。因为这些参数描述了开始和结束状态,所以您可以重载 operator() 以便为不同的开关执行不同的成员函数。

请注意状态 On 和 Off 是对象。 Boost.MetaStateMachine 提供了一个宏 BOOST_MSM_EUML_STATE_NAME 来获取状态的类型。如果您使用 C++11,则可以使用运算符 decltype 而不是宏。

switch_light 动作已通过 BOOST_MSM_EUML_ACTION 定义,在按下灯开关时执行。转换表已相应更改。第一个转换现在是 Off + press / switch_light == On。您在事件后的斜线后传递操作。此转换意味着如果当前状态为 Off 并且事件按下发生,则调用 switch_light 的运算符 operator()。执行操作后,新状态为开启。

示例 68.2 将 Switching on 然后 Switching off 写入标准输出。

示例 68.3。具有状态、事件、守卫和动作的状态机

#include <boost/msm/front/euml/euml.hpp>
#include <boost/msm/front/euml/state_grammar.hpp>
#include <boost/msm/back/state_machine.hpp>
#include <iostream>
namespace msm = boost::msm;
using namespace boost::msm::front::euml;
BOOST_MSM_EUML_STATE((), Off)
BOOST_MSM_EUML_STATE((), On)
BOOST_MSM_EUML_EVENT(press)
BOOST_MSM_EUML_ACTION(is_broken)
{
  template <class Event, class Fsm, class Source, class Target>
  bool operator()(const Event &ev, Fsm &fsm, Source &src, Target &trg) const
  {
    return true;
  }
};
BOOST_MSM_EUML_ACTION(switch_light)
{
  template <class Event, class Fsm, class Source, class Target>
  void operator()(const Event &ev, Fsm &fsm, Source &src, Target &trg) const
  {
    std::cout << "Switching\n";
  }
};
BOOST_MSM_EUML_TRANSITION_TABLE((
  Off + press [!is_broken] / switch_light == On,
  On + press / switch_light == Off
), light_transition_table)
BOOST_MSM_EUML_DECLARE_STATE_MACHINE(
(light_transition_table, init_ << Off),
light_state_machine)
int main()
{
  msm::back::state_machine<light_state_machine> light;
  light.process_event(press);
  light.process_event(press);
}

Example 68.3 

示例 68.3 在转换表中使用了一个守卫。第一个转换的定义是 Off + press [!is_broken] / switch_light == On。在括号中传递 is_broken 意味着状态机在调用动作 switch_light 之前检查是否可能发生转换。这叫做守卫。守卫必须返回布尔类型的结果。

像 is_broken 这样的守卫是用 BOOST_MSM_EUML_ACTION 定义的,其方式与动作相同。因此,必须为相同的四个参数重载运算符 operator()。 operator() 必须有一个 bool 类型的返回值才能用作守卫。

请注意,您可以使用运算符等逻辑运算符!在括号内的守卫上。

如果运行该示例,您会注意到没有任何内容写入标准输出。 switch_light 动作未执行 - 灯保持关闭状态。守卫 is_broken 返回 true。但是,因为运算符运算符!使用时,括号中的表达式的计算结果为 false。

您可以使用守卫来检查是否可以发生状态转换。示例 68.3 使用 is_broken 检查灯是否坏了。虽然从关闭到打开的转换通常是可能的,并且转换表正确地描述了灯,但在此示例中,灯无法打开。尽管调用了两次 process_event(),但灯的状态为关闭。

示例 68.4。具有状态、事件、进入动作和退出动作的状态机

#include <boost/msm/front/euml/euml.hpp>
#include <boost/msm/front/euml/state_grammar.hpp>
#include <boost/msm/back/state_machine.hpp>
#include <iostream>
namespace msm = boost::msm;
using namespace boost::msm::front::euml;
BOOST_MSM_EUML_ACTION(state_entry)
{
  template <class Event, class Fsm, class State>
  void operator()(const Event &ev, Fsm &fsm, State &state) const
  {
    std::cout << "Entering\n";
  }
};
BOOST_MSM_EUML_ACTION(state_exit)
{
  template <class Event, class Fsm, class State>
  void operator()(const Event &ev, Fsm &fsm, State &state) const
  {
    std::cout << "Exiting\n";
  }
};
BOOST_MSM_EUML_STATE((state_entry, state_exit), Off)
BOOST_MSM_EUML_STATE((state_entry, state_exit), On)
BOOST_MSM_EUML_EVENT(press)
BOOST_MSM_EUML_TRANSITION_TABLE((
  Off + press == On,
  On + press == Off
), light_transition_table)
BOOST_MSM_EUML_DECLARE_STATE_MACHINE(
(light_transition_table, init_ << Off),
light_state_machine)
int main()
{
  msm::back::state_machine<light_state_machine> light;
  light.process_event(press);
  light.process_event(press);
}

在示例 68.4 中,传递给 BOOST_MSM_EUML_STATE 的第一个参数是一个由 state_entry 和 state_exit 组成的元组。 state_entry 是进入动作,state_exit 是退出动作。这些动作在进入或退出状态时执行。

与操作一样,进入和退出操作是使用 BOOST_MSM_EUML_ACTION 定义的。但是,重载运算符 operator() 只需要三个参数:对事件的引用、状态机和状态。状态之间的转换对于进入和退出操作无关紧要,因此只需将一个状态传递给 operator()。对于进入动作,进入该状态。对于退出操作,此状态已退出。

在示例 68.4 中,状态 Off 和 On 都有进入和退出操作。因为事件按下发生了两次,所以 Entering 和 Exiting 显示了两次。请注意,Exiting 会先显示,Entering 会显示在后面,因为执行的第一个操作是退出操作。

第一个事件按下触发从关闭到开启的转换,退出和进入各显示一次。第二次事件按下将状态切换为关闭。 Exiting 和 Entering 再次显示一次。因此,状态转换首先执行退出动作,然后执行新状态的进入动作。

示例 68.5。状态机中的属性

#include <boost/msm/front/euml/euml.hpp>
#include <boost/msm/front/euml/state_grammar.hpp>
#include <boost/msm/back/state_machine.hpp>
#include <iostream>
namespace msm = boost::msm;
using namespace boost::msm::front::euml;
BOOST_MSM_EUML_DECLARE_ATTRIBUTE(int, switched_on)
BOOST_MSM_EUML_ACTION(state_entry)
{
  template <class Event, class Fsm, class State>
  void operator()(const Event &ev, Fsm &fsm, State &state) const
  {
    std::cout << "Switched on\n";
    ++fsm.get_attribute(switched_on);
  }
};
BOOST_MSM_EUML_ACTION(is_broken)
{
  template <class Event, class Fsm, class Source, class Target>
  bool operator()(const Event &ev, Fsm &fsm, Source &src, Target &trg) const
  {
    return fsm.get_attribute(switched_on) > 1;
  }
};
BOOST_MSM_EUML_STATE((), Off)
BOOST_MSM_EUML_STATE((state_entry), On)
BOOST_MSM_EUML_EVENT(press)
BOOST_MSM_EUML_TRANSITION_TABLE((
  Off + press [!is_broken] == On,
  On + press == Off
), light_transition_table)
BOOST_MSM_EUML_DECLARE_STATE_MACHINE(
(light_transition_table, init_ << Off, no_action, no_action,
attributes_ << switched_on), light_state_machine)
int main()
{
  msm::back::state_machine<light_state_machine> light;
  light.process_event(press);
  light.process_event(press);
  light.process_event(press);
  light.process_event(press);
  light.process_event(press);
}

Example 68.5 

示例 68.5 使用守卫 is_broken 检查是否可以从 Off 到 On 的状态转换。这次 is_broken 的返回值取决于电灯开关被按下的频率。可以在灯坏之前将灯打开两次。为了计算灯打开的频率,使用了一个属性。

属性是可以附加到对象的变量。它们允许您在运行时调整状态机的行为。因为诸如开灯频率之类的数据必须存储在某个地方,所以将其直接存储在状态机、状态或事件中是有意义的。

在使用属性之前,必须对其进行定义。这是通过宏 BOOST_MSM_EUML_DECLARE_ATTRIBUTE 完成的。传递给 BOOST_MSM_EUML_DECLARE_ATTRIBUTE 的第一个参数是类型,第二个是属性的名称。示例 68.5 定义了 int 类型的属性 switched_on。

定义属性后,必须将其附加到对象。该示例将属性 switched_on 附加到状态机。这是通过元组中的第五个值完成的,该值作为第一个参数传递给 BOOST_MSM_EUML_DECLARE_STATE_MACHINE。使用 attributes_,来自 Boost.MetaStateMachine 的关键字用于创建 lambda 函数。要将属性 switched_on 附加到状态机,请使用 operator<< 将 switched_on 写入 attributes_,就好像它是一个流一样。

元组中的第三个和第四个值都设置为 no_action。该属性作为元组中的第五个值传递。第三个和第四个值可用于定义状态机的进入和退出操作。如果没有定义进入和退出操作,请使用 no_action。

将属性附加到状态机后,可以使用 get_attribute() 访问它。在示例 68.5 中,此成员函数在入口操作 state_entry 中被调用以增加属性的值。因为 state_entry 仅链接到状态 On,switched_on 仅在灯打开时递增。

switched_on 也可以从守卫 is_broken 访问,它检查属性的值是否大于 1。如果是,守卫返回 true。由于属性是使用默认构造函数初始化的,并且 switched_on 设置为 0,如果灯已打开两次,is_broken 将返回 true。

在示例 68.5 中,事件按下发生了五次。灯被打开和关闭两次,然后再次打开。灯打开的前两次会显示“已打开”。但是,第三次打开灯时没有输出。发生这种情况是因为 is_broken 在灯被打开两次后返回 true,因此,没有从 Off 到 On 的状态转换。这意味着不执行状态 On 的进入操作,并且该示例不写入标准输出。

示例 68.6。访问转换表中的属性

#include <boost/msm/front/euml/euml.hpp>
#include <boost/msm/front/euml/state_grammar.hpp>
#include <boost/msm/back/state_machine.hpp>
#include <iostream>
namespace msm = boost::msm;
using namespace boost::msm::front::euml;
BOOST_MSM_EUML_DECLARE_ATTRIBUTE(int, switched_on)
void write_message()
{
  std::cout << "Switched on\n";
}
BOOST_MSM_EUML_FUNCTION(WriteMessage_, write_message, write_message_,
  void, void)
BOOST_MSM_EUML_STATE((), Off)
BOOST_MSM_EUML_STATE((), On)
BOOST_MSM_EUML_EVENT(press)
BOOST_MSM_EUML_TRANSITION_TABLE((
  Off + press [fsm_(switched_on) < Int_<2>()] / (++fsm_(switched_on),
    write_message_()) == On,
  On + press == Off
), light_transition_table)
BOOST_MSM_EUML_DECLARE_STATE_MACHINE(
(light_transition_table, init_ << Off, no_action, no_action,
attributes_ << switched_on), light_state_machine)
int main()
{
  msm::back::state_machine<light_state_machine> light;
  light.process_event(press);
  light.process_event(press);
  light.process_event(press);
  light.process_event(press);
  light.process_event(press);
}

Example 68.6 

示例 68.6 与示例 68.5 做同样的事情:将灯打开两次后,灯坏了,无法再打开。虽然前面的示例在操作中访问了 switched_on 属性,但此示例使用转换表中的属性。

Boost.MetaStateMachine 提供函数 fsm_() 来访问状态机中的属性。这样就定义了一个守卫来检查 switched_on 是否小于 2。并且定义了一个动作,每次状态从 Off 切换到 On 时都会增加 switched_on。

请注意,守卫中的小于比较是通过 Int_<2>() 完成的。数字 2 必须作为模板参数传递给 Int_ 以创建此类的实例。这将创建一个具有 Boost.MetaStateMachine 所需类型的函数对象。

示例 68.6 还使用宏 BOOST_MSM_EUML_FUNCTION 使函数成为动作。传递给 BOOST_MSM_EUML_FUNCTION 的第一个参数是可以在函数前端使用的动作的名称。第二个参数是函数的名称。第三个参数是在 eUML 中使用的操作名称。第四个和第五个参数是函数的返回值——一个用于动作用于状态转换的情况,另一个用于动作描述进入或退出动作的情况。以这种方式将 write_message() 转换为动作后,将在转换表中的 ++fsm_(switched_on) 之后创建并使用 write_message_ 类型的对象。在从 Off 到 On 的状态转换中,属性 switched_on 递增,然后调用 write_message()。

Example 68.6 

与示例 68.5 中一样,示例 68.6 显示两次打开。

Boost.MetaStateMachine 提供额外的函数,例如 state_() 和 event_(),以访问附加到其他对象的属性。其他类,例如 Char_ 和 String_,也可以像 Int_ 一样使用。

提示

正如您在示例中看到的,前端 eUML 要求您使用许多宏。头文件 boost/msm/front/euml/common.hpp 包含所有 eUML 宏的定义,这使其成为一个有用的参考。

练习

为可以关闭、打开或倾斜的窗口创建状态机。关闭的窗户可以打开或倾斜。如果不先关闭打开的窗户,则无法倾斜它。倾斜的窗户也不能在不先关闭的情况下打开。通过打开和倾斜您的窗口几次来测试您的状态机。使用 current_state() 将状态写入标准输出。

扩展状态机:窗户应该是智能家居的一部分。状态机现在应该计算窗户打开和倾斜的频率。要测试您的状态机,请打开并倾斜您的窗口几次。在程序结束时,将窗口打开的频率和倾斜的频率写入标准输出。

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们