命令模式核心结构是抽象基类command含execute()/undo(),具体命令持receiver引用;生命周期由智能指针或栈管理,状态快照需在execute()开头保存,撤销依赖准确记录而非重算,异步请求应由executor调度而非command自执行。

命令模式的核心结构怎么搭
命令模式本质是把「请求」封装成对象,让调用者(Invoker)和执行者(Receiver)解耦。C++里最直接的做法是定义抽象基类 Command,含纯虚函数 execute() 和可选的 undo();每个具体命令(如 LightOnCommand、SaveDocumentCommand)继承它,并持有对 Receiver 的引用或指针。
关键点不是“怎么写类”,而是“谁负责生命周期管理”:如果 Command 对象需要被反复重用或跨线程传递,必须用智能指针(std::shared_ptr<command></command>);若只在栈上短时存在(比如 GUI 按钮点击后立刻执行),直接传值或栈对象更轻量。
-
Receiver通常不继承接口,只是普通业务类,命令类内部调用其公有方法 - 避免在
Command构造时做耗时操作(如文件打开、网络连接),应延迟到execute()中 - 若需支持参数化命令(比如“撤销第3次保存”),把必要状态存为成员变量,而非依赖外部上下文
撤销功能为什么常出 bug
撤销不是简单地反向调用 undo() —— 它依赖命令对象能准确记录执行前的状态。常见错误是:在 execute() 中修改了 Receiver 的状态,但 undo() 试图恢复时发现原始值已丢失。
典型翻车场景:TextEditor::deleteSelection() 执行后删掉了文本,但 DeleteCommand 没保存被删内容,undo() 只能空白恢复。正确做法是在 execute() 开头就拷贝关键状态(如字符串片段、坐标、ID 列表)。
立即学习“C++免费学习笔记(深入)”;
- 不要在
undo()里重新计算原始值(比如“用当前光标位置反推上次删除范围”),不可靠 - 涉及资源(文件句柄、内存块),
undo()必须能安全释放或回滚,否则引发 double-free 或泄漏 - 复合命令(
MacroCommand)的undo()必须逆序执行子命令的undo(),顺序错会导致状态混乱
如何安全封装 C++ 请求(尤其是异步/多线程)
命令模式天然适合封装请求,但 C++ 原生不带协程或 await,所以“请求封装”往往指向两个方向:一是包装同步调用便于统一调度(如日志、重试、超时),二是桥接异步 API(如 std::async、回调函数、std::promise)。
重点在于隔离副作用:一个 HttpRequestCommand 对象不应自己启动线程,而应由专用的 CommandExecutor 负责调度。这样既能复用连接池,又能集中处理异常和超时。
- 异步命令的
execute()应返回std::future<void></void>或自定义 handle,而非阻塞等待 - 撤销网络请求时,不能只取消本地任务,要调用底层 cancel 接口(如
curl_easy_cleanup()或 HTTP client 的cancel()方法) - 避免在
Command里捕获std::exception_ptr后裸存——要用std::optional<:exception_ptr></:exception_ptr>显式标记是否出错
std::function 能替代 Command 类吗
可以,但不推荐用于需要撤销或持久化的场景。用 std::function<void></void> 封装执行逻辑很简洁,但它无法自然携带 undo 行为、无法序列化、无法查询类型信息,且闭包捕获的对象生命周期难管控。
例如:auto cmd = [&editor]() { editor.save(); }; —— 这个 lambda 如果被存入队列延后执行,而 editor 已析构,运行时崩溃不可避免。
- 仅当命令是临时、单次、作用域明确(如局部回调)时,
std::function是合理选择 - 需要撤销、重做、日志审计、跨模块传递时,必须用具名类继承
Command,并显式管理状态与资源 - 混合使用可行:让具体 Command 内部用
std::function存储执行逻辑,但自身仍承担生命周期、undo、序列化职责
真正麻烦的从来不是写几个虚函数,而是决定哪些状态该快照、哪些资源必须配对管理、以及在多线程下谁负责 delete 那个 new Command 出来的对象。











