如何通过命令模式核心逻辑实现系统撤销重做(UndoRedo)功能的源码编写?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1243个文字,预计阅读时间需要5分钟。
由于默认底层是`std::deque`,不支持迭代器遍历、无法随机访问历史节点,也无法在中间插入或回滚到任意位置——而实际编辑场景常需跳转到第5步或重复时跳过某条命令——因此,`std::stack`的`top()`返回的是const引用,无法安全地移动资源(如移动独占型命令对象)。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 改用
std::vector<:unique_ptr>></:unique_ptr>存储命令序列,保留索引位置和可遍历性 - 维护一个
m_currentIndex指向「当前已执行的最后一条命令」,初始为 -1(表示无任何执行) - Undo 时不是 pop,而是调用
command->undo()并递减索引;Redo 是递增索引后调用command->redo() - 新命令追加前,先清空
m_redoStack(即从m_currentIndex + 1到末尾的所有命令)
Command 基类必须支持 move semantics 才能避免深拷贝开销
很多初版实现把命令数据全量拷贝进 Command 对象,比如存一份 std::string 或 QImage 副本,导致频繁编辑时内存暴涨。根本原因是没定义移动构造函数和移动赋值运算符,编译器默认生成拷贝版本。
实操建议:
立即学习“C++免费学习笔记(深入)”;
-
Command基类成员尽量用std::unique_ptr或std::move-only类型封装状态(如std::unique_ptr<editdata></editdata>) - 显式声明
Command(Command&&) = default;和Command& operator=(Command&&) = default; - 派生类构造时用
std::move(data)转移资源,例如:TextInsertCommand::TextInsertCommand(std::string&& text, int pos) : m_text(std::move(text)), m_pos(pos) {} - 避免在
execute()中分配大对象;如有必要,延迟到 undo/redo 时再构造(用 lazy init)
如何安全处理命令执行失败后的状态一致性
常见错误是:命令 execute() 抛异常,但 m_currentIndex 已提前递增,导致 Undo 链断裂或重放错位。C++ 没有 finally 语义,必须靠 RAII 或显式检查。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 执行新命令前,先
push_back()到 vector,再调用execute();若抛异常,立刻pop_back()并重置索引 - 用 guard 类管理索引变更:
class ExecutionGuard { int& m_index; size_t m_prev; public: ExecutionGuard(int& idx, size_t new_idx) : m_index(idx), m_prev(idx) { m_index = static_cast<int>(new_idx); } ~ExecutionGuard() { if (m_index != static_cast<int>(m_prev)) m_index = static_cast<int>(m_prev); } };
- 所有命令的
execute()、undo()、redo()必须是 nothrow 或强异常安全(至少保证对象处于有效状态) - 对不可逆操作(如文件写入),应在
execute()前预检并返回bool,由调用方决定是否入栈
Qt 场景下 connect(QAction) 到 CommandFactory 的典型陷阱
直接把 QAction::triggered 绑定到 lambda 创建命令并执行,会导致命令对象生命周期失控:lambda 捕获的局部 std::unique_ptr 在函数退出后销毁,而 Undo 栈还持有着悬垂指针。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 命令创建必须由中心工厂(如
CommandFactory::createInsertCommand(...))统一完成,并返回已移交所有权的std::unique_ptr<command></command> - UI 触发逻辑只负责调用工厂 + 执行 + 入栈,不持有原始数据引用;文本编辑器中光标位置、选区等应打包进命令构造参数,而非捕获外部变量
- 若需响应实时输入(如按键连打),用
QTimer::singleShot(0, ...)合并多次触发,避免命令爆炸 - 注意 Qt 的信号槽连接类型:跨线程时用
Qt::QueuedConnection,否则命令可能在非主线程执行,破坏 UI 状态一致性
本文共计1243个文字,预计阅读时间需要5分钟。
由于默认底层是`std::deque`,不支持迭代器遍历、无法随机访问历史节点,也无法在中间插入或回滚到任意位置——而实际编辑场景常需跳转到第5步或重复时跳过某条命令——因此,`std::stack`的`top()`返回的是const引用,无法安全地移动资源(如移动独占型命令对象)。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 改用
std::vector<:unique_ptr>></:unique_ptr>存储命令序列,保留索引位置和可遍历性 - 维护一个
m_currentIndex指向「当前已执行的最后一条命令」,初始为 -1(表示无任何执行) - Undo 时不是 pop,而是调用
command->undo()并递减索引;Redo 是递增索引后调用command->redo() - 新命令追加前,先清空
m_redoStack(即从m_currentIndex + 1到末尾的所有命令)
Command 基类必须支持 move semantics 才能避免深拷贝开销
很多初版实现把命令数据全量拷贝进 Command 对象,比如存一份 std::string 或 QImage 副本,导致频繁编辑时内存暴涨。根本原因是没定义移动构造函数和移动赋值运算符,编译器默认生成拷贝版本。
实操建议:
立即学习“C++免费学习笔记(深入)”;
-
Command基类成员尽量用std::unique_ptr或std::move-only类型封装状态(如std::unique_ptr<editdata></editdata>) - 显式声明
Command(Command&&) = default;和Command& operator=(Command&&) = default; - 派生类构造时用
std::move(data)转移资源,例如:TextInsertCommand::TextInsertCommand(std::string&& text, int pos) : m_text(std::move(text)), m_pos(pos) {} - 避免在
execute()中分配大对象;如有必要,延迟到 undo/redo 时再构造(用 lazy init)
如何安全处理命令执行失败后的状态一致性
常见错误是:命令 execute() 抛异常,但 m_currentIndex 已提前递增,导致 Undo 链断裂或重放错位。C++ 没有 finally 语义,必须靠 RAII 或显式检查。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 执行新命令前,先
push_back()到 vector,再调用execute();若抛异常,立刻pop_back()并重置索引 - 用 guard 类管理索引变更:
class ExecutionGuard { int& m_index; size_t m_prev; public: ExecutionGuard(int& idx, size_t new_idx) : m_index(idx), m_prev(idx) { m_index = static_cast<int>(new_idx); } ~ExecutionGuard() { if (m_index != static_cast<int>(m_prev)) m_index = static_cast<int>(m_prev); } };
- 所有命令的
execute()、undo()、redo()必须是 nothrow 或强异常安全(至少保证对象处于有效状态) - 对不可逆操作(如文件写入),应在
execute()前预检并返回bool,由调用方决定是否入栈
Qt 场景下 connect(QAction) 到 CommandFactory 的典型陷阱
直接把 QAction::triggered 绑定到 lambda 创建命令并执行,会导致命令对象生命周期失控:lambda 捕获的局部 std::unique_ptr 在函数退出后销毁,而 Undo 栈还持有着悬垂指针。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 命令创建必须由中心工厂(如
CommandFactory::createInsertCommand(...))统一完成,并返回已移交所有权的std::unique_ptr<command></command> - UI 触发逻辑只负责调用工厂 + 执行 + 入栈,不持有原始数据引用;文本编辑器中光标位置、选区等应打包进命令构造参数,而非捕获外部变量
- 若需响应实时输入(如按键连打),用
QTimer::singleShot(0, ...)合并多次触发,避免命令爆炸 - 注意 Qt 的信号槽连接类型:跨线程时用
Qt::QueuedConnection,否则命令可能在非主线程执行,破坏 UI 状态一致性

