在C和C++中实现有限状态机(FSM)并不像理解它们那样简单。尽管FSM的概念相对直观,但编写相关的代码时,常常不得不面对繁琐的模板元编程和重复的代码。为了简化这一过程,开发了一个库,旨在提供一个简洁的接口,同时避免编写大量的样板代码。
本文将首先介绍一个简单的随机数生成器状态机的示例,然后展示如何使用状态机对象,并解释如何设计状态机类。
尽管FSM的概念简单明了,但在C和C++中实现它们却并非易事。作者一直希望有一个简单的库来帮助编写FSM,同时避免编写重复的代码。几年前,作者提供了一个类似的库,但它并不遵循C++标准,依赖于编译器,最终作者放弃了使用它。现在,作者希望有一个更好的库实现。
将从一个简单的随机数生成器状态机的图表开始,然后展示如何使用状态机对象,最后解释如何设计状态机类。
图表展示了一个简单的随机数生成器,它有两个状态:On和Off。初始状态是Off。有三种转换:start、stop和generate。在On状态下,可以省略"start"转换,因为"start"的目的是将机器打开。但是,为了清晰起见,还是画出了它。
在图表的某个地方,会生成一个随机数,但是不清楚这个数字保存在哪里。为了明确,这个数字保存在状态机内部的数据上下文中(用户提供的结构)。这个数据也可以被任何希望观察状态机的人访问。
现在知道了状态机的样子,也知道了定义状态机的三个关键特性。让使用它!上述示例已经通过本文提供的库创建好了。请打开demo fsm.zip文件,并使用CMake为喜欢的编译器或IDE生成项目。
当程序员使用状态机时,他的代码应该看起来像这样:
fsm<NumbersGeneratorTransitions> numbersGenerator;
numbersGenerator.move_to_state<StateOff>();
printf("\nData %X\n", numbersGenerator.state().number);
// 将显示未初始化的数字
numbersGenerator->Start();
numbersGenerator->Generate();
numbersGenerator->Stop();
numbersGenerator->Generate();
printf("\nData %d\n", numbersGenerator.state().number);
// 将显示生成的随机数
请注意,demo fsm.zip中的示例与上述示例类似,但更详细,涵盖了其他一些情况。
对于想要了解幕后实现细节的人来说,作者专门为此部分提供了介绍。
FSM模板接口会要求提供状态转换接口基类(实际的接口)。
template <typename InterfaceT> class fsm
FSM上下文类会再次要求接口和状态类,但永远不会直接访问这个类。它只是作为FSM类的超模板类标记。
template<typename InterfaceT, typename StateT> class fsm_ctx
提供了转换接口的基类。它为转换接口提供了一些服务,包括静态构造函数,某种代理类来处理状态和其他样板代码。
template <typename InterfaceT, typename StateT> class impl_ctx
在开发这个库时,作者试图避免不必要的内存分配,这会导致大量的内存碎片。基本上,在每个状态变化时,新状态的内存应该被分配。不幸的是,如果只使用常规的new操作符来完成这项工作,最终会得到内存碎片。为了避免这种情况,为第一个状态转换接口分配内存,一旦完成并且移动到下一个状态,相同的内存地址将被重用于下一个状态转换接口,除非下一个状态转换接口类比已经分配在内存中的那个大,在这种情况下,将为它分配一个新的内存空间。这样,内存分配在FSM的生命周期内保持静态,与每个状态变化的数千或数百万次内存分配相比。在大多数情况下(和编译器),所有接口的大小预计是相同的,这导致非常优化的内存使用(在这种情况下只分配了一块内存)。
状态转换的构造和析构在状态变化时自动完成(对于用户来说,当然,对于库的开发者来说,这是一个非常手动的过程)。这意味着用户可以使用每个转换接口的构造函数来做一些事情,也可以使用析构函数来清理东西。
如果用户试图在初始化之前使用FSM,将在该事件上抛出异常。