Welcome back! In our last session, we mastered the art of transforming raw user input into a smooth, variable-width SkVertices mesh. We can now render beautiful strokes, but our application is currently a one-way street—we can add content, but we can't take it away. This brings us to a fundamental feature of any interactive creation tool: the ability to undo and redo actions.
In this lesson, we will architect the system that makes this possible. You will learn how to design a general-purpose Command interface and a HistoryManager to track user actions, applying the powerful Command design pattern. This architecture is not just for undoing strokes; it will form a robust foundation for managing every state-changing action in your application, from adding strokes and text to moving and deleting objects.
The Problem with Direct State Mutation
Currently, when a user draws a stroke, you might be inclined to simply add the resulting SkVertices object to a std::vector of things to be rendered. This is straightforward, but it poses a significant problem for undoing. To undo the "add stroke" action, you would need to remove that specific SkVertices object from the vector. But how do you put it back for a "redo"? And what was the state before you added it? What if the action was more complex, like modifying an existing stroke?
Directly modifying your application's state makes it very difficult to reverse those changes. The solution is to stop thinking about actions as direct manipulations and start thinking of them as self-contained, reversible transactions.
The Command Design Pattern
The Command pattern is a behavioral design pattern that encapsulates a request as an object. Instead of an object invoking a method directly on another object, it creates a Command object that contains all the information needed to perform the action. This decouples the object that issues the request from the object that performs the action.
For our purposes, the most significant benefit is that this Command object can also hold the information needed to reverse the action.
The key components are:
- Command: An interface declaring methods for executing and, crucially, undoing an action.
- Concrete Command: A specific implementation of the
Commandinterface, representing one type of action (e.g.,AddStrokeCommand,MoveObjectCommand). - Receiver: The object that the command acts upon (e.g., our main canvas or document object that holds the list of all strokes).
- Invoker: The object that triggers the command and manages the history (our
HistoryManager).
This UML diagram shows a typical structure. The CommandInvoker holds a list of Command objects, and Client code creates concrete commands (DrawCircle, DrawRectangle) that operate on a Receiver (ShapeDrafter).

Designing the Command Interface
At its core, our Command interface in C++ will be an abstract base class. Any action that can be undone must inherit from it.
class Command {
public:
virtual ~Command() = default;
// Performs the action
virtual void execute() = 0;
// Reverts the action
virtual void undo() = 0;
};
A concrete command, like one for adding a stroke, will store a reference to the Receiver (e.g., the main data model of our whiteboard) and the data needed for the action (the stroke itself).
execute(): Would add the stroke to the receiver's list of strokes.undo(): Would remove the stroke from the receiver's list.
The video below from Mike Shah gives an excellent C++-centric overview of the pattern, including the motivation and the implementation of the base Command class with execute() and undo().
Design Patterns - Command Pattern Explanation and Implementation in C++
This video provides a clear introduction to the Command pattern in C++, explaining its purpose and walking through a live-coded example.
Pay close attention to these sections: The introduction explains the purpose of the pattern, specifically mentioning its use for undo/redo systems. This walkthrough uses a controller analogy to explain how different actions can be encapsulated and stored in a history. The C++ base class section shows the creation of the abstract Command class with pure virtual execute and undo functions. The Move command implementation shows how a concrete command inherits from the base class and starts implementing the logic.
Managing the Command History
Now that we can encapsulate actions, how do we manage them? The standard approach is to use two stacks: an "undo stack" and a "redo stack".

The logic works as follows:
- New Action: When a user performs an action (e.g., draws a stroke), we create a new
Commandobject, callexecute()on it, and push it onto theundostack. Crucially, any time a new action is performed, theredostack is cleared. This creates a linear history. - Undo: When the user requests an undo, we pop the most recent command from the
undostack, call itsundo()method, and push it onto theredostack. - Redo: When the user requests a redo, we pop the most recent command from the
redostack, call itsexecute()method again, and push it back onto theundostack.
The following article gives a fantastic, in-depth look at the philosophy and mechanics of building an undo system. We will focus on the core mechanics it describes.
Undo, the art of – Part 1 - Max Liani - WordPress.com
This article by Max Liani provides a great conceptual foundation for undo systems. It explains the two-stack model clearly.
Please read the section "Basics of Undo". It describes the interaction between the undo and redo stacks, which is the exact model we will be implementing.
A High-Performance Command Implementation
The execute()/undo() model is very common. However, for a high-performance system, it's worth considering a slightly different perspective. Instead of thinking about "doing" and "undoing" an action, we can think about storing the "before" and "after" state.
In this model, a command object stores the state of the data before the change and the state after the change.
- The
undo()method simply restores the "before" state. - The
redo()method (which can just beexecute()) restores the "after" state.
This approach is more robust because it doesn't rely on the logic of the action being perfectly reversible. It simply swaps data. This aligns well with a data-oriented design philosophy focused on state transformation, which is key in performance-critical systems.
The video below from GamesWithGabe provides an excellent, pragmatic walkthrough of building an undo/redo system with this philosophy. It shows how the Command object stores the old value before applying the new one, making the undo operation a simple restoration.
The Command Pattern: Coding Undo/Redo | Game Engine Concepts #3
This video offers a game developer's perspective on the Command pattern, focusing on a practical and robust implementation for undo/redo.
Watch these key parts: What is the Command Pattern? introduces the core concept of encapsulating data modifications. The Command History explains the two-stack model with a current pointer. Implementing Undo and Redo is the most important section. It details how to store the "old" state within the command object to enable a simple undo() call, and how execute() can be reused for redo(). Handling New Commands explains the crucial rule: when a new command is added after some undos, the "future" (the redo stack) must be cleared.
Designing the HistoryManager
Let's translate this design into a C++ class that will manage our command history. We will use std::vector to act as our stacks, and std::unique_ptr to manage the memory of the command objects, preventing leaks.
#include <vector>
#include <memory>
// Forward declaration of our Command interface
class Command;
class HistoryManager {
public:
HistoryManager() = default;
// Executes a new command and adds it to the history.
void doCommand(std::unique_ptr<Command> command) {
// Clear the redo stack, as we are starting a new branch of history.
// This is a critical step for a linear undo/redo system.
redo_stack_.clear();
command->execute();
undo_stack_.push_back(std::move(command));
}
void undo() {
if (!canUndo()) {
return;
}
// Move the command from the undo stack to the redo stack.
std::unique_ptr<Command> command = std::move(undo_stack_.back());
undo_stack_.pop_back();
command->undo();
redo_stack_.push_back(std::move(command));
}
void redo() {
if (!canRedo()) {
return;
}
// Move the command from the redo stack back to the undo stack.
std::unique_ptr<Command> command = std::move(redo_stack_.back());
redo_stack_.pop_back();
command->execute(); // `execute()` serves as our redo.
undo_stack_.push_back(std::move(command));
}
bool canUndo() const {
return !undo_stack_.empty();
}
bool canRedo() const {
return !redo_stack_.empty();
}
private:
// Using std::vector as a stack is common and efficient.
std::vector<std::unique_ptr<Command>> undo_stack_;
std::vector<std::unique_ptr<Command>> redo_stack_;
};
This HistoryManager class provides a clean and robust interface for the rest of your application. Your UI code for handling Ctrl+Z and Ctrl+Y would simply call historyManager.undo() and historyManager.redo().
Conclusion
In this lesson, we have designed a powerful and flexible architecture for managing state changes in your application. By decoupling actions from their execution, we've laid the essential groundwork for a full-featured undo/redo system.
Key Takeaways:
- The Command design pattern encapsulates an action and its parameters into a single object.
- A
Commandinterface should provide methods to bothexecute()andundo()an action. - A robust implementation stores the necessary "before" and "after" state within the command object, making
undoa simple state restoration. - A
HistoryManageruses two stacks (for undo and redo history) to manage the sequence of commands. - When a new command is executed, the redo history must be cleared to ensure a consistent, linear timeline.
- Using smart pointers like
std::unique_ptris crucial for managing the lifetime of command objects and preventing memory leaks.
In our next lesson, we'll put this theory into practice. We will implement our first concrete command: AddStrokeCommand. This command will encapsulate the data (SkVertices) and logic for adding a new stroke to the canvas, making our drawing actions fully undoable.