命令模式的核心是将“调用”封装为对象,解耦“何时执行”与“执行什么”,适用于排队、撤销、重试等场景;推荐优先使用 std::function + lambda,仅在需统一生命周期管理或复杂 undo 时才引入类层次。

命令模式的核心不是类图,是把“调用”变成对象
命令模式在 C++ 里最容易被写成过度封装的样板代码——比如为每个操作新建一个继承自 Command 的空壳子类,再加一堆 execute()、undo() 虚函数,结果连个文件读取都得绕三层。它真正该解决的问题只有一个:把“什么时候做”和“做什么”拆开,尤其适合需要排队、撤销、重试或延迟执行的场景。
实操建议:
- 别一上来就定义抽象基类
Command;先看你的“命令”是不是能用std::function<void></void>直接装下 - 如果命令需要捕获状态(比如某个
Widget*或int id),直接用 lambda 捕获,比手写继承类快且不易出错 - 只有当需要统一管理生命周期(比如命令对象要长期持有、跨线程传递)或必须支持
undo()且状态复杂时,才值得上类层次
用 std::function + lambda 实现最简命令队列
常见错误现象:std::function 捕获局部变量后,在队列里调用时报 segmentation fault 或值全变零——本质是 lambda 捕获了栈地址,而命令入队后原作用域已结束。
使用场景:按钮点击回调、任务延时调度、日志批量提交
立即学习“C++免费学习笔记(深入)”;
实操建议:
- 捕获指针时确认对象生命周期长于命令队列;更安全的做法是捕获
std::shared_ptr(如[ptr = shared_from_this()](){ ptr->do_something(); }) - 避免按值捕获大对象;改用
[data = std::move(data)]() mutable { ... }显式转移所有权 - 命令队列本身用
std::vector<:function>></:function>即可,不用硬套std::queue——后者不支持随机遍历和批量清空
示例:
std::vector<std::function<void()>> queue;
int x = 42;
queue.push_back([x]() { std::cout << x << "\n"; }); // OK:按值捕获
Widget* w = new Widget;
queue.push_back([w]() { w->render(); }); // 危险:w 可能已被 delete
带 undo 的命令类怎么避免内存泄漏和状态错乱
参数差异:纯执行命令只需保存“做什么”,带 undo() 的命令必须同时保存“刚才做了什么”。很多人只存反向操作函数,却忘了存原始数据(比如删除前的文本内容、移动前的位置)。
性能影响:每次执行都要 new 一个命令对象,又在 undo 后 delete,频繁触发堆分配;若命令高频发生(如编辑器光标移动),会明显拖慢响应。
实操建议:
- 把命令对象设计成可复用的结构体,用
std::unique_ptr管理,或直接放在对象池里(如std::array<command></command>+ 自由链表) -
undo()不应抛异常;若撤销失败(如文件已被删),应静默失败并记录日志,否则整个撤销链会中断 - 不要在
execute()里调用undo()做测试——这会污染真实状态;用独立的test_undo()方法验证
示例(简化版):
struct TextDeleteCommand {
std::string text_before;
size_t pos;
Editor* editor;
void execute() { editor->delete_range(pos, text_before.length()); }
void undo() { editor->insert_at(pos, text_before); } // 必须存原始文本,不能只存“删了5个字符”
};
为什么 std::bind 在命令模式里基本可以淘汰了
常见错误现象:用 std::bind(&Widget::click, w, 1, _1) 绑定成员函数,结果传入参数类型不匹配,编译报一屏 template argument deduction 错误;或者绑定后对象析构了,调用时崩溃。
兼容性影响:C++11 起 std::bind 就比 lambda 多一层间接调用,GCC/Clang 对 lambda 内联优化更激进;MSVC 在 /O2 下对 bind 的优化也弱于 lambda。
实操建议:
- 所有原本想用
std::bind的地方,直接写 lambda:[w](int x) { w->click(x); }更短、更清晰、更容易调试 - 需要延迟绑定参数?用
[w, x](int y) { w->click(x + y); },而不是嵌套 bind - 唯一例外:要适配旧接口要求
std::function参数类型严格匹配(比如 Qt 的QMetaObject::invokeMethod),才考虑 bind
命令最难的部分从来不是定义接口,而是决定哪些状态该进命令对象、哪些该由外部保证存活——这点没想清楚,后面所有撤销、重做、序列化都会出问题。











