命令模式核心在于决定执行权归属,而非单纯写类;应避免为封装而封装,优先用std::function+lambda解耦,但需警惕捕获悬空引用。

命令模式核心不是写类,而是决定谁持有执行权
命令模式在 C++ 里最容易写成“为封装而封装”——一堆 Command 子类,但调用方仍要手动 new、传参、调用 execute(),和直接调函数没区别。关键在于:**谁该负责保存命令对象?谁触发执行?是否支持撤销?** 这些决定了你到底需要几层抽象。
常见错误是把 Receiver(真正干活的对象)塞进每个 Command 实例里,导致命令对象体积大、生命周期难管理。正确做法是让 Command 持有轻量级句柄(如 std::shared_ptr 或 ID),执行时再查表或转发。
- 如果只是解耦调用和实现(比如 UI 按钮绑定动作),用
std::function<void></void>+ lambda 更轻量,不必硬套类图 - 需要撤销/重做,
Command必须保存足够上下文(如原值、目标 ID),不能只记操作类型 - 避免在
execute()里做耗时操作;若需异步,命令对象得自己管理线程安全,别依赖调用方
std::function 能替代 Command 类,但要注意捕获陷阱
很多场景下,std::function<void></void> 就是更现代的命令对象。它天然支持闭包,能捕获局部变量、this 指针,省去手写子类的样板代码。
但容易踩的坑是悬空引用:lambda 捕获了栈变量或临时对象,而命令被存到队列或延后执行,运行时访问已销毁内存。错误示例如下:
立即学习“C++免费学习笔记(深入)”;
auto cmd = [&value]() { std::cout << value; }; // value 是局部变量解决方法只有两个:
- 用值捕获
[=],确保所有数据被拷贝(注意大对象拷贝开销) - 用智能指针捕获堆对象,如
[ptr = std::make_shared<int>(42)]() { ... }</int> - 绝对不要用
[&]捕获局部变量,除非你 100% 确保命令生命周期不超作用域
Undo/Redo 不是加个 inverse() 就完事
很多人给 Command 加个 undo() 虚函数,以为就支持撤销了。实际问题在状态一致性:执行 A 命令后,系统状态变了,undo A 必须精准还原到执行前那一刻的状态,而不是“反向操作”。比如 “删除文件” 的 undo 不是 “新建同名文件”,而是从回收站恢复原始 inode 和权限。
所以真正可靠的 undo 要求:
-
execute()前必须快照关键状态(如对象字段、容器 size、指针值),不能靠推理逆运算 - 快照成本高时,改用 Memento 模式分离状态存储,命令只持 memento 句柄
- 多个命令组合(MacroCommand)必须原子性:要么全部 execute,要么全部不执行;undo 同理,不能只 undo 其中几个
多线程下命令队列不是加个 mutex 就安全
把命令 push 到队列、worker 线程 pop 执行,看似简单。但常见问题是:命令对象内部持有非线程安全资源(如 std::vector、裸指针、文件句柄),而执行线程和构造线程不同,导致竞态。
根本解法不是锁整个队列,而是约束命令对象的契约:
- 命令对象构造完成后,必须是**不可变的**(const-correct),所有可变状态应在
execute()内部通过线程本地资源完成 - 若需共享状态,显式用
std::shared_mutex或无锁结构(如moodycamel::ConcurrentQueue),而非对std::queue加锁 - 避免在
execute()中调用可能阻塞或回调到主线程的 API(如 Qt 的QMetaObject::invokeMethod),这会隐式引入线程切换依赖
最常被忽略的一点:命令对象析构时机。如果 worker 线程还在执行,而命令被外部提前释放(比如用户取消操作),必须确保 execute() 能检测到“已取消”并安全退出,而不是访问 dangling 成员。










