C++20之设计模式:状态模式
状态模式
- 状态模式
- 状态驱动的状态机
- 手工状态机
- Boost.MSM 中的状态机
- 总结
状态模式
我必须承认:我的行为是由我的状态支配的。如果我没有足够的睡眠,我会有点累。如果我喝了酒,我就不会开车了。所有这些都是状态(states),它们支配着我的行为:我的感受,我能做什么,我不能做什。
当然,我可以从一种状态转换到另一种状态。我可以去喝杯咖啡,它能让我从瞌睡中清醒过来(我希望如此!)所以我们可以把咖啡当作触发器,让你真正从困倦过渡到清醒。这里,让我笨拙地为你解释一下:
coffee
sleepy ----------> alert
所以,状态设计模式是一个非常简单的想法:状态控制行为;状态可以改变;唯一的问题是谁引发了状态的变更。
基本上有两种方式:
- 状态是带有行为的实际类,这些行为将实际状态从一个转换到另一个
- 状态和转换只是枚举。我们有一个称为
状态机(state machine)
的特殊组件,它执行实际的转换。
这两种方法都是可行的,但实际上第二种方法是最常见的。这两种我们都会过一遍,但我必须承认我只会简单浏览第一个,因为这不是人们通常做事情的方式。
状态驱动的状态机
我们将从最简单的例子开始:一个电灯开关。它只能处于开和关的状态。我们将构建一个任何状态都能够切换到其他状态的模型:虽然这反映了状态设计模式的经典实现(根据GoF的书),但我并不推荐这样做。
首先,让我们为电灯开关建模:它只有一种状态和一些从一种状态切换到另一种状态的方法:
class LightSwitch
{State* state;public:LightSwitch(){state = new OffState();}void set_state(State* state){this->state = state;}
};
这一切看起来都很合理。我们现在可以定义状态,在这个特定的情况下,它将是一个实际的类:
struct State
{virtual void on(LightSwitch* ls){cout << "Light is already on\n";}virtual void off(LightSwitch* ls){cout << "Light is already off\n";}
};
这个实现很不直观,所以我们需要慢慢地仔细地讨论它,因为从一开始,关于State类的任何东西都没有意义。
首先,State
不是抽象的!你会认为一个你没有办法(或理由)达到的状态是抽象的。但事实并非如此。
第二,状态允许从一种状态切换到另一种状态。这对一个通情达理的人来说,毫无意义。想象一下电灯开关:它是改变状态的开关。人们并不指望State
本身会改变自己,但它似乎就是这样做的。
第三,也许是最令人困惑的,State::on/off
的默认行为声称我们已经处于这种状态!在我们实现示例的其余部分时,这一点将在某种程度上结合在一起。
现在我们实现On
和Off
状态:
struct OnState : State
{OnState() { cout << "Light turned on\n"; }void off(LightSwitch* ls) override;
};struct OffState : State
{OffState() { cout << "Light turned off\n"; }void on(LightSwitch* ls) override;
};
实现OnState::off和OffState::on允许状态本身切换到另一个状态!它看起来是这样的:
void OnState::off(LightSwitch* ls)
{cout << "Switching light off...\n";ls->set_state(new OffState());delete this;
} // same for OffState::on
这就是转换发生的地方。这个实现包含了对delete This
的奇怪调用,这在真实的c++中是不常见的。这对初始分配状态的位置做出了非常危险的假设。例如,可以使用智能指针重写这个示例,但是使用指针和堆分配清楚地表明状态在这里被积极地销毁。如果状态有析构函数,它将触发,你将在这里执行额外的清理。
当然,我们确实希望开关本身也能切换状态,就像这样:
class LightSwitch
{...void on() { state->on(this); }void off() { state->off(this); }
};
因此,把所有这些放在一起,我们可以运行以下场景:
1 LightSwitch ls; // Light turned off
2 ls.on(); // Switching light on...
3 // Light turned on
4 ls.off(); // Switching light off...
5 // Light turned off
6 ls.off(); // Light is already off
我必须承认:我不喜欢这种方法,因为它不是直观的。当然,状态可以被告知(观察者模式)我们正在进入它。但是,状态转换到另一种状态的想法——根据GoF的书,这是状态模式的经典实现——似乎不是特别令人满意。
如果我们笨拙地说明从OffState
到OnState
的转换,则需要将其说明为
LightSwitch::on() -> OffState::on()
OffState -------------------------------------> OnState
另一方面,从OnState到OnState的转换使用基状态类,这个类告诉你你已经处于那个状态
LightSwitch::on() -> State::on()
OnState ----------------------------------> OnState
这里给出的示例可能看起来特别人为,所以我们现在将看看另一个手工创建的设置,其中的状态和转换被简化为枚举成员。
手工状态机
让我们尝试为一个典型的电话会话定义一个状态机。首先,我们将描述电话的状态:
enum class State
{off_hook,connecting,connected,on_hold,on_hook
};
我们现在还可以定义状态之间的转换,也可以定义为enum class
:
enum class Trigger
{call_dialed,hung_up,call_connected,placed_on_hold,taken_off_hold,left_message,stop_using_phone
};
现在,这个状态机的确切规则,即可能的转换,需要存储在某个地方。
map<State, vector<pair<Trigger, State>>> rules;
这有点笨拙,但本质上map
的键是我们移动的状态,值是一组表示Trigger-State
的对,在此状态下可能的触发器以及使用触发器时所进入的状态。
让我们来初始化这个数据结构:
rules[State::off_hook] = {{Trigger::call_dialed, State::connecting},{Trigger::stop_using_phone, State::on_hook}
};rules[State::connecting] = {{Trigger::hung_up, State::off_hook},{Trigger::call_connected, State::connected}
};
// more rules here
我们还需要一个启动状态,如果我们希望状态机在达到该状态后停止执行,我们还可以添加一个退出(终止)状态:
State currentState { State::off_hook },
exitState { State::on_hook };
完成这些之后,我们就不必为实际运行(我们使用orchestrating
这个术语)状态机而构建单独的组件了。例如,如果我们想要构建电话的交互式模型,我们可以这样做:
while(true)
{cout << "The phone is currently " << currentState << endl;select_trigger:cout << "Select a trigger:" << "\n";int i = 0;for(auto &&item : rules[currentState]){cout << i++ << ". " << item.first << "\n";}int input;cin >> input;for(input < 0 || (input+1) > rules[currentState].size()){goto select_trigger;}currentState = rules[currentState][input].second;if(currentState == exitState) break;
}
首先:是的,我确实使用goto
,这是一个很好的例子,说明在什么地方使用goto
是合适的(译者注:一般不建议在程序里面使用goto,这样会使得程序的控制流比较混乱)。对于算法本身,这是相当明显的:我们让用户在当前状态上选择一个可用的触发器(operator<<
状态和触发器都在幕后实现了),并且,如果触发器是有效的,我们通过使用前面创建的规则映射转换到它。
如果我们到达的状态是退出状态,我们就跳出循环。下面是一个与程序交互的示例。
1 The phone is currently off the hook
2 Select a trigger:
3 0. call dialed
4 1. putting phone on hook
5 0
6 The phone is currently connecting
7 Select a trigger:
8 0. hung up
9 1. call connected
10 1
11 The phone is currently connected
12 Select a trigger:
13 0. left message
14 1. hung up
15 2. placed on hold
16 2
17 The phone is currently on hold
18 Select a trigger:
19 0. taken off hold
20 1. hung up
21 1
22 The phone is currently off the hook
23 Select a trigger:
24 0. call dialed
25 1. putting phone on hook
26 1
27 We are done using the phone
这种手工状态机的主要优点是非常容易理解:状态和转换是普通的枚举类,转换集是在一个简单的std::map
中定义的,开始和结束状态是简单的变量
Boost.MSM 中的状态机
在现实世界中,状态机要复杂得多。有时,你希望在达到某个状态时发生某些操作。在其他时候,你希望转换是有条件的,也就是说,你希望转换只在某些条件存在时发生。
当Boost.MSM (Meta State Machine)
,一个状态机库,是Boost的一部分,你的状态机是一个通过CRTP
继承自state_ machine_def
的类:
struct PhoneStateMachine : state_machine_def<PhoneStateMachine>
{bool angry{ false };
}
我添加了一个bool
变量来指示调用者是否angry
(例如,在被搁置时); 我们稍后会用到它。现在,每个状态也可以驻留在状态机中,并且可以从state
类继承:
struct OffHook : state<> {};
struct Connecting : state<>
{template<class Event, class FSM>void on_entry(Event const& evt, FSM&){cout << "We are connecting..." << endl;}// also on_exit
};
// other states omitted
如你所见,状态还可以定义在进入或退出特定状态时发生的行为。你也可以定义在转换时执行的行为(而不是当你到达一个状态时):这些也是类,但它们不需要从任何东西继承;相反,它们需要提供具有特定签名的operator()
:
struct PhoneBeingDestoryed
{template<class EVT, class FSM, class SourceState, class TargetState>void operator()(EVT const&, FSM& SourceState&, TargetState&){cout << "Phone breaks into a million pieces" << endl;}
};
正如你可能已经猜到的那样,这些参数提供了对状态机的引用,以及你将要进入和进入的状态。
最后,我们有守卫条件(guard condition
):这些条件决定我们是否可以在第一时间使用一个转换。现在,我们的布尔变量angry
不是MSM
可用的形式,所以我们需要包装它:
struct CanDestoryPhone
{template<class EVT, class FSM, class SourceState, class TargetState>bool operator()(EVT const&, FSM& fsm, SourceState&, TargetState&){return fsm.angry;}
};
前面的例子创建了一个名为CanDestroyPhone
的守卫条件,稍后我们可以在定义状态机时使用它。
为了定义状态机规则,Boost.MSM
使用MPL(元编程库)。具体来说,转换表被定义为mpl::vector
,每一行依次包含:
- 源状态
- 状态转换
- 目标状态
- 一个要执行的可选操作
- 一个可选守卫条件
因此,有了所有这些,我们可以像下面这样定义一些电话呼叫规则:
struct transition_table : mpl::vector<Row<OffHook, CallDialed, Connecting>,Row<Connecting, CallConnected, Connected>,Row<Connected, PlacedOnHold, OnHold>,Row<OnHold, PhoneThrownIntoWall, PhoneDestoryed, PhoneBeingDestoryed, CanDestoryPhone>
>
{};
在前面的方法中,与状态不同,CallDialed
之类的转换是可以在状态机类之外定义的类。它们不必继承自任何基类,而且很容易为空,但它们必须是类型。
transition_table
的最后一行是最有趣的:它指定我们只能尝试在CanDestroyPhone
保护条件下销毁电话,并且当电话实际上被销毁时,应该执行PhoneBeingDestroyed
操作。
现在,我们可以添加更多的东西。首先,我们添加起始条件:因为我们正在使用Boost.MSM
,起始条件是一个类型定义,而不是一个变量:
typedef OffHook initial_state;
最后,如果没有可能的转换,我们可以定义要发生的操作。它可能发生!比如,你把手机摔坏了,就不能再用了,对吧?
template <class FSM, class Event>
void no_transition(Event const& e, FSM&, int state)
{cout << "No transition from state " << state_names[state]<< " on event " << typeid(e).name() << endl;
}
Boost MSM
将状态机分为前端(我们刚刚写的)和后端(运行它的部分)。使用后端API,我们可以根据前面的状态机定义构造状态机:
msm::back::state_machine<PhoneStateMachine> phone;
现在,假设存在info()
函数,它只打印我们所处的状态,我们可以尝试orchestrating
以下场景
1 info(); // The phone is currently off hook
2 phone.process_event(CallDialed{}); // We are connecting...
3 info(); // The phone is currently connecting
4 phone.process_event(CallConnected{});
5 info(); // The phone is currently connected
6 phone.process_event(PlacedOnHold{});
7 info(); // The phone is currently on hold
8 9
phone.process_event(PhoneThrownIntoWall{});
10 // Phone breaks into a million pieces
11
12 info(); // The phone is currently destroyed
13
14 phone.process_event(CallDialed{});
15 // No transition from state destroyed on event struct CallDialed
因此,这就是定义更复杂、具有工业强度的状态机的方式。
总结
首先,这是值得强调的Boost.MSM
是Boost中两种状态机实现之一,另一种是Boost.statechart
。我很确定还有很多其他的状态机实现。
其次,状态机的功能远不止这些。例如,许多库支持分层状态机的思想:例如,一个生病(Sick)
的状态可以包含许多不同的子状态,如流感(Flu)
或水痘(Chickenpox)
。如果你在处于感染流感的状态,你也同时处于生病的状态。
最后,有必要再次强调现代状态机与状态设计模式的原始形式之间的差异。重复api的存在(例如LightSwitch::on/off vs. State::on/off
)以及自删除的存在在我的书中是明确的代码气味。不要误会我的方法是有效的,但它是不直观的和繁琐的。
相关文章:
C++20之设计模式:状态模式
状态模式 状态模式状态驱动的状态机手工状态机Boost.MSM 中的状态机总结 状态模式 我必须承认:我的行为是由我的状态支配的。如果我没有足够的睡眠,我会有点累。如果我喝了酒,我就不会开车了。所有这些都是状态(states),它们支配着我的行为:…...

数据库安全综合治理方案(可编辑54页PPT)
引言:数据库安全综合治理方案是一个系统性的工作,需要从多个方面入手,综合运用各种技术和管理手段,确保数据库系统的安全稳定运行。 方案介绍: 数据库安全综合治理方案是一个综合性的策略,旨在确保数据库系…...

人工智能:大语言模型提示注入攻击安全风险分析报告下载
大语言模型提示注入攻击安全风险分析报告下载 今天分享的是人工智能AI研究报告:《大语言模型提示注入攻击安全风险分析报告》。(报告出品方:大数据协同安全技术国家工程研究中心安全大脑国家新一代人工智能开放创新平台) 研究报告…...
【购买源码时有许多需要注意的坑】
购买源码时有许多需要注意的“坑”,这些坑可能会对项目的后续开发和使用造成严重影响。以下是一些需要特别注意的方面: 源码的完整性 编译测试:确保到手的源码能够从头至尾编译、打包、部署和功能测试无误。这一步非常关键,因为只…...
CAS的三大问题和解决方案
一、ABA问题的解决方案 变量第一次读取的值是1,后来其他线程改成了3,然后又被其他线程修改成了1,原来期望的值是第一个1才会设置新值,第二个1跟期望不符合,但是,可以设置新值。 解决方案: &a…...
EDA和统计分析有什么区别
EDA(Electronic Design Automation)和统计分析在多个方面存在显著的区别,这些区别主要体现在它们的应用领域、目的、方法以及所使用的工具上。 EDA(电子设计自动化) 定义与目的: EDA是电子设计自动化&…...
CentOS 7 修改DNS
1、nmcli connection show 命令找到设备名称 # nmcli connection show NAME UUID TYPE DEVICE enp4s0 99559edf-4e0a-4bae-a528-6d75065261e9 ethernet enp4s0 2、nmcli connection modify 命令修改dns nmcli connection modif…...
PHP基础语法-Part2
if-else语句、switch语句 与其他语言相同 循环结构 for循环while循环do-while循环foreach循环,搭配数组使用 foreach ($age as $avlue) //只输出值 {xxx; } foreach ($age as $key > $avlue) //键和值都输出 {xxx; }foreach ($age as $key >…...

数据结构门槛-顺序表
顺序表 1. 线性表2. 顺序表2.1 静态顺序表2.2 动态顺序表2.2.1 动态数据表初始化和销毁2.2.2 动态数据表的尾插尾删2.2.3 动态数据表的头插头删2.2.4 动态数据表的中间部分插入删除2.2.5 动态数据表的查询数据位置 3. 总结 1. 线性表 线性表(linear list࿰…...
软件测试面试准备工作
1、 什么是数据库? 答:数据库是按照某种数据模型组织起来的并存放二级存储器中的数据集合。 2、 什么是关系型数据库? 答:关系型数据库是建立在关系数据库模型基础上的数据库, 借助集合代数等概念和方法处理数据库中的数据。目前主流的关…...

Java面试八股之后Spring、spring mvc和spring boot的区别
Spring、spring mvc和spring boot的区别 Spring, Spring Boot和Spring MVC都是Spring框架家族的一部分,它们各自有其特定的用途和优势。下面是它们之间的主要区别: Spring: Spring 是一个开源的轻量级Java开发框架,最初由Rod Johnson创建&…...
linux对齐TOF和RGB摄像头画面
问题:TOF和RGB画面不对齐 linux同时接入TOF和RGB,两者出图时间是由驱动层控制(RGB硬件触发出图),应用层只负责读取数据。 现在两者画面不对齐,发现是开始的时候两者出图数量不一致导致的。底层解决不了&a…...

配置linux客户端免密登录服务端linux主机的root用户
1、客户端与服务端的ip 客户端IP地址服务端IP地址 2、定位客户端,由客户端制作公私钥对 [rootclient ~]# ssh-keygen -t rsa (RSA是非对称加密算法) # 一路回车 3、定位客户端,将公钥上传到服务器端root账户 [rootc…...

SpringMVC实现文件上传
导入文件上传相关依赖 <!--文件上传--> <dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.3.1</version> </dependency> <dependency><groupId>…...

计算机实验室排课查询小程序的设计
管理员账户功能包括:系统首页,个人中心,学生管理,教师管理,实验室信息管理,实验室预约管理,取消预约管理,实验课程管理,实验报告管理,报修信息管理࿰…...

分享几种电商平台商品数据的批量自动抓取方式
在当今数字化时代,电商平台作为商品交易的重要渠道,其数据对于商家、市场分析师及数据科学家来说具有极高的价值。批量自动抓取电商平台商品数据成为提升业务效率、优化市场策略的重要手段。本文将详细介绍几种主流的电商平台商品数据批量自动抓取方式&a…...

mysql面试(五)
前言 本章节从数据页的具体结构,分析到如何生成索引,如何构成B树的索引结构。 以及什么是聚簇索引,什么是联合索引 InnoDB数据结构 行数据 我看各种文档中有好多记录数据结构的,但是这些都是看完就忘的东西。在这里详细讲也没…...
微软全球蓝屏带来的思考及未来战争走向
微软全球蓝屏事件不仅揭示了技术层面的问题和挑战,还引发了对未来战争走向的一些深入思考。以下是关于这些思考的内容: 微软全球蓝屏带来的思考: 系统稳定性与安全性:微软全球蓝屏事件凸显了操作系统稳定性和安全性的重要性。一…...

以FastGPT为例提升Rag知识库应用中的检索召回命中率
提升Rag知识库应用中的检索召回命中率 在构建Rag(Retrieval-Augmented Generation)知识库应用时,检索召回知识片段的命中率是至关重要的。高效、准确的检索机制是确保AI系统能够精准响应用户查询的基础。当前,FastGPT主要采用三种…...

ffmpeg更改视频的帧率
note 视频帧率调整 帧率(fps-frame per second) 例如:原来帧率为30,调整后为1 现象:原来是每秒有30张图像,调整后每秒1张图像,看着图像很慢 实现:在每秒的时间区间里,取一张图像…...

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)
题目:3442. 奇偶频次间的最大差值 I 思路 :哈希,时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况,哈希表这里用数组即可实现。 C版本: class Solution { public:int maxDifference(string s) {int a[26]…...

Prompt Tuning、P-Tuning、Prefix Tuning的区别
一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...
进程地址空间(比特课总结)
一、进程地址空间 1. 环境变量 1 )⽤户级环境变量与系统级环境变量 全局属性:环境变量具有全局属性,会被⼦进程继承。例如当bash启动⼦进程时,环 境变量会⾃动传递给⼦进程。 本地变量限制:本地变量只在当前进程(ba…...

通过Wrangler CLI在worker中创建数据库和表
官方使用文档:Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后,会在本地和远程创建数据库: npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库: 现在,您的Cloudfla…...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...

关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...

微信小程序云开发平台MySQL的连接方式
注:微信小程序云开发平台指的是腾讯云开发 先给结论:微信小程序云开发平台的MySQL,无法通过获取数据库连接信息的方式进行连接,连接只能通过云开发的SDK连接,具体要参考官方文档: 为什么? 因为…...

七、数据库的完整性
七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...