在C++中实现状态机 怎么做?

3

我是C++的新手。

如何在C++中实现状态机?

我只能得到消息,但需要知道下一个状态。

我需要使用什么样的结构?

谢谢,Igal


8
你目前有什么进展? - Björn Pollex
12个回答

19

对于简单状态机,您可以在循环中使用switch语句,例如:

for (;;)
{
    switch (state)
    {

    case STATE_1:
        // do stuff
        // maybe change state
        break;

    case STATE_2:
        // do stuff
        // maybe change state
        break;

    case STATE_3:
        // do stuff
        // maybe change state
        break;

    // ...

    }
}

2
仅适用于非常简单的状态机。它根本不是面向对象的。您的代码仅限于C,不使用C ++。 - Peter K.
13
@Peter K.:他并没有要求使用面向对象的解决方案,也没有说明他需要使用特定于C ++的语言特性。由于上面的代码可以在C++(以及C和Objective C)中编译,我认为它符合所述要求。鉴于这是家庭作业,我假设只需要一个非常简单的状态机。 - Paul R
2
@Peter K.:美在观者眼中。 :-) - Paul R
这引出了一个问题,即简单是否就是美。 - John K
1
@Lorenzo:下一个状态是由当前状态的逻辑决定的,即与当前状态匹配的case标签的代码。 - Paul R
1
@Paul R:谢谢您的回答。您的代码非常有趣。我希望能够编写整个解决方案并在这里发布(当然,如果它可以运行的话;-)。 - Igal Spector

7
typedef std::pair<State,Message> StateMessagePair;
typedef std::map<StateMessagePair,State> StateDiagram;
StateDiagram sd;
// add logic to diagram
...
State currentState = getInitialState();
...
// process message
Message m = getMessage();
StateDiagram::iterator it=sd.find(std::make_pair(currentState,m)));
if (it==sd.end()) exit("incorrect message");
currentState = it->second;

编辑: 构建状态图的方法如下(以可乐售货机为例):

StateDiagram.insert(std::make_pair(State::Idle           ,Message::MakeChoice   ),State::WaitingForMoney);
StateDiagram.insert(std::make_pair(State::WaitingForMoney,Message::Cancel       ),State::Idle);
StateDiagram.insert(std::make_pair(State::WaitingForMoney,Message::MoneyEntered ),State::FindCan);
StateDiagram.insert(std::make_pair(State::FindCan        ,Message::CanSentToUser),State::Idle);

默认操作可以使用第二个映射来实现,其中键仅为State,如下所示:

typedef std::map<State,State> StateDiagramForDefaults;

逻辑可以在StateDiagramForDefaults中进行查找,而不是打印“错误消息”。

如果需要将操作添加到状态图中,则映射的值应该是一个由动作和新状态组成的对,如下所示:

typedef std::pair<State,Message> StateMessagePair;
typedef std::pair<State,IAction *> StateActionPair;
typedef std::map<StateMessagePair,StateActionPair> StateDiagram;

构建图表的逻辑应该“new”一个实现IAction接口的类的实例,并将其放入StateDiagram中。
执行逻辑然后通过虚拟方法(例如execute()或()运算符)执行IAction实现。

我的一些同事喜欢使用这种方式构建状态机(实际上更好一点:他们不使用std::pair,而是嵌套使用std::maps)。看起来很酷,很面向对象,但我的问题是在这样的实现中,我们会失去对状态转换表实际样貌的视角。更不用说为某个状态添加默认操作可能会是一个很大的烦恼了。 - Dummy00001
我认为状态转移表会变得更加清晰,而不是变得更加模糊。如果你有一个明确的地方将配对添加到映射中,那么你也可以清楚地了解所有的转换。默认操作可以使用单独的映射来实现。我稍后会在我的答案中澄清这两件事。 - Patrick
我认为你的第一个代码块中应该是 std::make_pair(currentState,m) - Amr Saber
@Argento,确实。谢谢您注意到这一点。已经更正了代码。 - Patrick

6
标准状态机实现技术包括:
  1. 嵌套switch语句(一些先前的帖子展示了这种技术的示例)
  2. 状态表
  3. GoF状态设计模式
  4. 以上技术的组合
如果您是初学状态机和C或C++实现的人,我建议阅读我的Dr.Dobbs文章“回归基础”,可在http://www.ddj.com/184401737上找到(您需要点击顶部的打印链接方便地阅读文本)。
以上标准技术都不适用于分层状态机(例如UML状态图)。如果您对现代UML状态机感兴趣,我建议阅读我在Embedded.com上的三部分文章“UML状态机速成班”(http://www.embedded.com/design/testissue/215801043)。

@ Miro:感谢您向我介绍这个网站。看起来非常有趣和信息丰富。再次感谢。 - Igal Spector
截至2023年03月06日,《Dr Dobbs》文章已经出了大问题。此外,embedded.com的网址也无法使用了。 - oPless
请在谷歌上搜索我的名字,您会发现大量关于状态机的资源,包括我在PDF中的书籍、GitHub上的代码、YouTube上的视频等。 - Miro Samek

4
除非你只是为了实现一个状态机而实现它,否则我强烈建议你使用一个状态机生成器。Ragel是一个非常好的、稳定的解决方案。

https://github.com/bnoordhuis/ragel


2
一种方法是使用这样的类(以下是大致的示例代码):

class State
{
  //pass a new Message into the current State
  //current State does (state-specific) processing of
  //the message, and returns a pointer to the new State
  //if there's a state change
  virtual State* getNewState(const Message&) = 0;
};

class ExampleState
{
  virtual State* getNewState(const Message& message)
  {
    switch (message.type)
    {
      case MessageType.Stop:
        //change state to stopped
        return new StoppedState();
    }
    //no state change
    return 0;
  }
};

其中一个复杂之处在于状态应该是静态的轻量级模式,还是它们应该携带实例数据,因此应该被新建和删除。


另一个可能的复杂之处是实现超级状态和子状态,这允许某些消息在所有子状态中具有相同的处理方式,通过在超级状态中实现该消息的处理来实现。 - ChrisW
另一种可能性是让Message类拥有一个纯虚的State* getNewState(const State& oldState)方法,这样消息就知道状态而不是状态知道消息。第三种可能性是拥有一个二维数组,其中每个可能的消息/状态组合都有一个单独的元素,每个元素都是一个函数指针,用于处理特定的消息/状态组合。 - ChrisW
@ ChrisW,感谢您的回答。 您的代码非常有趣,而且非常接近我所需要的。 我打算编写完整的程序并在这里发布。再次感谢。 - Igal Spector

2
哎呀,这并不像看起来那么复杂。状态机代码非常简单、短小。
编写状态机只是微不足道的事情。难点在于设计一个在所有可能情况下都能正确行为的状态机。
但让我们假设您已经有了正确的设计。那么如何编写它呢?
1. 将状态存储在属性中,例如 myState。 2. 每当您接收到一条消息时,切换到 myState 属性以执行该状态的代码。 3. 在每个状态中,根据消息切换以执行该状态和消息的代码。
因此,您需要一个嵌套的 switch 语句。
cStateMachine::HandleMessage( msg_t msg )
{
  switch( myState ) {

     case A:

        switch( msg ) {

           case M:

           // here is the code to handle message M when in state A

...

一旦您成功运行此功能,添加更多的状态和消息就变得非常有趣和容易。


2

谢谢你推荐给我这个网站。看起来非常有趣和信息丰富。再次感谢。 - Igal Spector
虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。仅有链接的答案如果链接页面发生更改可能会变得无效。 - Paul R
死链 - EvilTeach

1

1
当然,Boost 为您准备了一些东西:Boost 状态图库。您还可以在那里找到一些不错的教程。

1

我正在使用的是基于这个的:机器对象

它似乎被很好地优化了,而且使用起来比Boost Statechart简单得多。


感谢您向我推荐这个网站。看起来非常有趣和信息丰富。再次感谢。 - Igal Spector
我们自2009年起在一个Windows应用程序(Kickdrive)中使用macho,该程序大量使用分层状态和多线程并发处理。对我们来说运行得非常好。真的值得点赞。 - Oliver Heggelbacher

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接