C++怎么实现命令模式_C++行为封装教程【解耦】

1次阅读

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

C++怎么实现命令模式_C++行为封装教程【解耦】

命令模式核心不是写类,而是决定谁持有执行权

命令模式在 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(如 qtQMetaObject::invokeMethod),这会隐式引入线程切换依赖

最常被忽略的一点:命令对象析构时机。如果 worker 线程还在执行,而命令被外部提前释放(比如用户取消操作),必须确保 execute() 能检测到“已取消”并安全退出,而不是访问 dangling 成员。

text=ZqhQzanResources