The Command Pattern in C++: Undo/Redo, Macros, Transactions and When It Is Overkill
Key takeaways
How the Command pattern turns requests into objects in C++, implementing undo/redo and macro systems, transactional commands, GUI and game input use cases, and signs you do not need it.
What is the Command Pattern? Why is it needed?
Problem Scenario: Implementing Undo
Problem: To implement Undo/Redo in a text editor, you need to record all actions and execute them in reverse order. If actions are implemented as function calls only, recording them becomes difficult.
// Bad example: function calls only
void insertText(std::string& doc, const std::string& text) {
doc += text;
// How to implement Undo?
}
Solution: The Command Pattern encapsulates requests as objects. Each Command has execute() and undo() methods and is stored in a history stack.
// Good example: Command object
class InsertCommand : public Command {
public:
InsertCommand(std::string& doc, const std::string& text)
: doc_(doc), text_(text), position_(doc.size()) {}
void execute() override {
doc_ += text_;
}
void undo() override {
doc_.erase(position_, text_.size());
}
private:
std::string& doc_;
std::string text_;
size_t position_;
};
flowchart TD
invoker["Invoker (Editor)"]
cmd[Command]
insert[InsertCommand]
delete[DeleteCommand]
receiver["Receiver (Document)"]
invoker -->|execute| cmd
insert -.->|implements| cmd
delete -.->|implements| cmd
insert --> receiver
delete --> receiver
The diagram above hints at the real reason this pattern exists, and it is easy to miss if you only look at the code: the arrow from the invoker points at an abstract Command, not at InsertCommand or DeleteCommand directly. That single indirection is the entire point. Without it, your RemoteControl, your toolbar button, your keyboard shortcut handler, and your macro recorder would each need a direct reference to Document and would each need to know which of its methods to call with which arguments. Every new action you add to the editor would mean touching every one of those call sites. With the Command Pattern, the invoker only ever calls execute() on whatever object it was handed — it does not know, and does not need to know, that the object is going to call insert() on a Document, send a packet over a socket, or mutate an in-memory game entity. The receiver-specific logic lives entirely inside the concrete command, and the invoker’s job shrinks down to “hold a command, call it, maybe remember it.” This is what people mean when they say the pattern “decouples the invoker from the receiver” — it is not an abstract OOP platitude, it is the difference between N invokers each hardcoding M receiver calls (an N×M coupling problem) versus N invokers and M commands that both only depend on one shared interface.
That decoupling is also why the same command object can be wired up to more than one invoker without duplicating logic. A “Bold” command in a text editor can be triggered from a toolbar button, a menu item, a keyboard shortcut, and a macro script, and all four invokers share the exact same execute()/undo() implementation. If bold formatting were instead four separate onClick handlers each calling editor.applyBold() directly, you would eventually get subtle divergence — one handler forgets to push onto the undo stack, another handler applies the formatting to the wrong selection range. Centralizing the behavior in one Command class removes that class of bug by construction.
Basic Structure
Minimal Command
#include <iostream>
#include <memory>
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
class Light {
public:
void on() { std::cout << "Light ON\n"; }
void off() { std::cout << "Light OFF\n"; }
};
class LightOnCommand : public Command {
public:
LightOnCommand(Light& light) : light_(light) {}
void execute() override { light_.on(); }
void undo() override { light_.off(); }
private:
Light& light_;
};
class LightOffCommand : public Command {
public:
LightOffCommand(Light& light) : light_(light) {}
void execute() override { light_.off(); }
void undo() override { light_.on(); }
private:
Light& light_;
};
class RemoteControl {
public:
void setCommand(std::unique_ptr<Command> cmd) {
command_ = std::move(cmd);
}
void pressButton() {
if (command_) {
command_->execute();
}
}
void pressUndo() {
if (command_) {
command_->undo();
}
}
private:
std::unique_ptr<Command> command_;
};
int main() {
Light light;
RemoteControl remote;
remote.setCommand(std::make_unique<LightOnCommand>(light));
remote.pressButton(); // Light ON
remote.pressUndo(); // Light OFF
}
Lambdas and std::function vs. a Class Hierarchy
Once you have written two or three of these tiny command classes, the boilerplate becomes obvious: a header, a constructor that stores a reference or two, and two one-line overrides. It is tempting — and often correct — to replace the whole hierarchy with std::function<void()> pairs:
struct FunctionCommand {
std::function<void()> doIt;
std::function<void()> undoIt;
};
FunctionCommand lightOn{
[&light]{ light.on(); },
[&light]{ light.off(); }
};
This trades a virtual call and a heap-allocated object for a type-erased callable, and for a lot of GUI code that is a fair trade: less code, no need to declare a new class for every button, and the capture list makes the dependency on light explicit at the call site instead of buried in a constructor. But the trade has real costs that only show up once the command count or the call frequency grows. std::function almost always performs its own heap allocation once your captured state exceeds the small-buffer-optimization threshold (commonly around 16 bytes on typical implementations, though the exact figure is implementation-defined) — capture two or three data members by value and you are allocating on every single command construction. A hand-written class hierarchy, by contrast, lets you control allocation explicitly: pool the command objects, reuse them, or allocate them on the stack when they do not outlive the current scope. std::function also erases type information, which makes some operations — comparing two commands for equality, serializing a command to replay it later, introspecting a command for a debugger or a macro editor’s “step list” — considerably harder than with a concrete class you can dynamic_cast or inspect through RTTI. My rule of thumb: reach for std::function when commands are short-lived, low-frequency, and only ever need to run once (menu actions, one-off button callbacks). Reach for a class hierarchy the moment you need undo, need to serialize the command, need to compose commands into macros, or need to run thousands of them per second in a hot loop.
Implementing Undo/Redo
History Stack
#include <iostream>
#include <memory>
#include <stack>
#include <string>
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
class Document {
public:
void insert(const std::string& text) {
content_ += text;
std::cout << "Document: " << content_ << '\n';
}
void remove(size_t pos, size_t len) {
content_.erase(pos, len);
std::cout << "Document: " << content_ << '\n';
}
const std::string& getContent() const { return content_; }
private:
std::string content_;
};
class InsertCommand : public Command {
public:
InsertCommand(Document& doc, const std::string& text)
: doc_(doc), text_(text), position_(doc.getContent().size()) {}
void execute() override {
doc_.insert(text_);
}
void undo() override {
doc_.remove(position_, text_.size());
}
private:
Document& doc_;
std::string text_;
size_t position_;
};
class CommandManager {
public:
void executeCommand(std::unique_ptr<Command> cmd) {
cmd->execute();
undoStack_.push(std::move(cmd));
// Clear Redo stack
while (!redoStack_.empty()) {
redoStack_.pop();
}
}
void undo() {
if (!undoStack_.empty()) {
auto cmd = std::move(undoStack_.top());
undoStack_.pop();
cmd->undo();
redoStack_.push(std::move(cmd));
}
}
void redo() {
if (!redoStack_.empty()) {
auto cmd = std::move(redoStack_.top());
redoStack_.pop();
cmd->execute();
undoStack_.push(std::move(cmd));
}
}
private:
std::stack<std::unique_ptr<Command>> undoStack_;
std::stack<std::unique_ptr<Command>> redoStack_;
};
int main() {
Document doc;
CommandManager manager;
manager.executeCommand(std::make_unique<InsertCommand>(doc, "Hello "));
manager.executeCommand(std::make_unique<InsertCommand>(doc, "World"));
manager.undo(); // "Hello "
manager.undo(); // ""
manager.redo(); // "Hello "
manager.redo(); // "Hello World"
}
The Real Memory Cost of a History Stack
The example above is deceptively cheap: InsertCommand only stores a string and a position, so the undo stack grows by a few dozen bytes per keystroke. Real applications rarely get off that easy. A “delete selection” command in a graphics editor has to remember what was deleted in order to restore it, and if the selection is a 4K image layer, “what was deleted” can be tens of megabytes. If you naively push one of these onto an unbounded std::stack for every user action, you have built a slow memory leak that only manifests after a long editing session — which is exactly the kind of bug that survives code review and QA and then shows up as a support ticket from a user who has had the app open for six hours.
There are two structurally different ways to keep undo history affordable, and choosing between them is a real design decision, not a formality. The first is delta-based undo: store only the difference between before and after (the inserted text, the deleted range, the changed pixels within a dirty rectangle) and replay or invert the delta to move between states. This is what the InsertCommand/DeleteCommand pair above already does, and it is why text editors can keep thousands of undo steps in memory without breaking a sweat — each delta is proportional to the size of the edit, not the size of the document. The second is snapshot-based undo: store a full copy of the relevant state before the change and swap it back in on undo. Snapshots are simpler to implement correctly (there is no risk of an inverse operation drifting out of sync with the forward operation), but they scale with the size of the state, not the size of the change, which is why they are usually paired with copy-on-write data structures, structural sharing (as in persistent data structures or Git’s tree objects), or a hard cap on history depth — the maxHistory_ pattern shown later in this article exists specifically to bound the worst case of a snapshot-heavy undo stack.
I once tracked down a slow, session-length memory leak in a diagram editor that came down to exactly this trade-off gone wrong. The “delete node” command captured a full clone of the node’s rendering cache — a QImage-equivalent buffer — inside its undo() payload, because that was the easiest way to guarantee undo could restore the exact rendered appearance without recomputing it. It worked, correctness-wise, on every unit test we had. The problem only appeared in the field: users who deleted and redrew nodes hundreds of times over a multi-hour session accumulated hundreds of these cached buffers in the undo stack, and since the stack had no size limit, memory climbed until the process was killed by the OS on lower-spec machines. The fix was not clever — cap the undo history at a sane depth (we settled on 200 steps after watching real session logs) and, more importantly, stop caching the rendering buffer in the command at all; regenerating it on undo cost a few milliseconds, which was imperceptible, versus the unbounded memory growth, which was not. The lesson that stuck with me is that “what does undo need to restore correctness” and “what is convenient to capture right now” are two different questions, and conflating them is how you end up with commands that quietly hoard memory.
Macro Systems
Composite Command
#include <iostream>
#include <memory>
#include <vector>
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
class MacroCommand : public Command {
public:
void add(std::unique_ptr<Command> cmd) {
commands_.push_back(std::move(cmd));
}
void execute() override {
for (auto& cmd : commands_) {
cmd->execute();
}
}
void undo() override {
// Undo in reverse order
for (auto it = commands_.rbegin(); it != commands_.rend(); ++it) {
(*it)->undo();
}
}
private:
std::vector<std::unique_ptr<Command>> commands_;
};
class PrintCommand : public Command {
public:
PrintCommand(const std::string& msg) : message_(msg) {}
void execute() override {
std::cout << message_ << '\n';
}
void undo() override {
std::cout << "Undo: " << message_ << '\n';
}
private:
std::string message_;
};
int main() {
auto macro = std::make_unique<MacroCommand>();
macro->add(std::make_unique<PrintCommand>("Step 1"));
macro->add(std::make_unique<PrintCommand>("Step 2"));
macro->add(std::make_unique<PrintCommand>("Step 3"));
macro->execute();
// Step 1
// Step 2
// Step 3
macro->undo();
// Undo: Step 3
// Undo: Step 2
// Undo: Step 1
}
Composite Commands in Practice
MacroCommand is a small but genuine application of the Composite design pattern layered on top of Command: because MacroCommand itself implements the Command interface, it can be nested inside another MacroCommand, and the invoker never has to know whether it is holding a single atomic action or a tree of a hundred nested ones. This is exactly how “macro recording” features work in real tools — Photoshop Actions, Excel macros, and IDE “record keystrokes and replay” features are all, structurally, an invoker that pushes every executed command into a MacroCommand while recording is on, then replays that composite as a single unit later. The undo semantics fall out naturally: undoing a macro means undoing its children in reverse order, which is precisely the loop shown above, and it is the same reasoning a database uses when rolling back a multi-statement transaction.
The part that is easy to gloss over is what happens when one sub-command in the middle of a macro fails partway through execution. The naive execute() loop above has no failure handling at all — if commands_[3]->execute() throws, commands 4 through N never run, but 0 through 3 already did, and now the receiver is in a state that corresponds to neither “before the macro” nor “after the macro.” For a “print three lines” macro that is harmless. For a macro that moves funds between five accounts or resizes and re-tags fifty files, it is a correctness bug waiting to happen, and it is the reason the Transaction pattern in the next section exists as a distinct, more disciplined variant of the same composite idea: it explicitly defines what “partial failure” means and rolls back everything that already ran, rather than leaving the receiver half-mutated.
Transactions
All-or-Nothing
#include <iostream>
#include <memory>
#include <vector>
#include <stdexcept>
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
class Transaction {
public:
void add(std::unique_ptr<Command> cmd) {
commands_.push_back(std::move(cmd));
}
bool commit() {
try {
for (auto& cmd : commands_) {
cmd->execute();
}
return true;
} catch (const std::exception& e) {
std::cerr << "Transaction failed: " << e.what() << '\n';
rollback();
return false;
}
}
void rollback() {
for (auto it = commands_.rbegin(); it != commands_.rend(); ++it) {
try {
(*it)->undo();
} catch (...) {
// Ignore rollback failures
}
}
}
private:
std::vector<std::unique_ptr<Command>> commands_;
};
class Account {
public:
Account(double balance) : balance_(balance) {}
void deposit(double amount) {
balance_ += amount;
std::cout << "Deposited $" << amount << ", Balance: $" << balance_ << '\n';
}
void withdraw(double amount) {
if (balance_ < amount) {
throw std::runtime_error("Insufficient funds");
}
balance_ -= amount;
std::cout << "Withdrew $" << amount << ", Balance: $" << balance_ << '\n';
}
private:
double balance_;
};
class DepositCommand : public Command {
public:
DepositCommand(Account& acc, double amount) : account_(acc), amount_(amount) {}
void execute() override { account_.deposit(amount_); }
void undo() override { account_.withdraw(amount_); }
private:
Account& account_;
double amount_;
};
class WithdrawCommand : public Command {
public:
WithdrawCommand(Account& acc, double amount) : account_(acc), amount_(amount) {}
void execute() override { account_.withdraw(amount_); }
void undo() override { account_.deposit(amount_); }
private:
Account& account_;
double amount_;
};
int main() {
Account acc(100.0);
Transaction txn;
txn.add(std::make_unique<WithdrawCommand>(acc, 50.0));
txn.add(std::make_unique<DepositCommand>(acc, 30.0));
txn.add(std::make_unique<WithdrawCommand>(acc, 100.0)); // Failure
if (!txn.commit()) {
std::cout << "Transaction rolled back\n";
}
}
Notice the crucial difference from MacroCommand: rollback() here catches every exception from undo() and swallows it, on purpose. If undo itself is allowed to throw and abort the rollback loop, you can end up half-rolled-back, which is worse than the original partial-execution problem — at least a fully-forward-partial-execution state is deterministic, whereas a partially-rolled-back state depends on exactly which sub-command’s undo happened to throw. This is a genuinely subtle piece of exception-safety design, and it is worth internalizing as a general pattern: rollback code must be written to never fail outright, even at the cost of “best effort, log and move on,” because the alternative (an exception escaping a rollback) leaves you with no coherent state to reason about at all. Real transactional systems (database engines, distributed sagas) invest heavily in making the compensating/rollback action itself as close to unconditionally-succeeding as they can — often by making it operate on already-known-good data rather than by re-deriving state.
GUI and Game Input Use Cases
The two domains where the Command Pattern shows up constantly in production C++ are GUI toolkits and game input handling, and it is worth being explicit about why, because the underlying motivation is slightly different in each.
In GUI frameworks (Qt’s QAction, MFC’s command routing via ON_COMMAND maps, Windows’ WM_COMMAND messages), the same logical action is almost always reachable from several places at once — a toolbar icon, a menu entry, a keyboard accelerator, and sometimes a context menu, all mapped to one “Save Document” command. Representing that action as a single Command object (or, in Qt’s case, a QAction that already bundles enabled/disabled state, icon, and shortcut alongside the triggered signal) means enabling or disabling the action in one place automatically disables it everywhere it is exposed, and the actual save logic is written and tested exactly once. This is the decoupling benefit from the introduction taken to its logical conclusion: N UI entry points, one command.
In games, the motivating problem is different: input needs to be rebindable, replayable, and sometimes transmitted over a network, none of which work well if “pressing jump” is hardcoded as a direct function call from the input handler to the physics code. Representing input as Command objects (or, in simpler engines, as small POD structs tagged with an action enum and serialized parameters) buys you rebindable controls for free — the physical key or button maps to a command type, not to a hardcoded call, so remapping is just changing which command a given input triggers. It also gives you deterministic replay and rollback netcode almost as a side effect: if you record the stream of input commands rather than snapshotting the entire game state every frame, you can replay a match byte-for-byte by feeding the same command stream back through the same simulation, and rollback networking (used in most modern fighting games) works by rewinding simulation state and re-applying a corrected command stream from the point of divergence. That only works because commands are small, serializable, and deterministic — which is also exactly why a fat std::function-based command with non-trivial captured state is a poor fit for this use case, and why competitive-latency-sensitive input systems in games tend to use plain structs or a lightweight hand-rolled hierarchy rather than type-erased callables.
That second point is where I learned a fairly painful lesson about std::function overhead firsthand. An input queue I worked on originally represented every player action as a std::function<void()> capturing a handful of parameters — an entity ID, a target position, a couple of floats. It was clean to read and quick to write. Under a profiler, during a stress test with dozens of simulated players generating input every frame, a noticeable chunk of frame time was going into heap allocation and deallocation churn from constructing and destroying these std::function objects tens of thousands of times per second — the captured state was just large enough to blow past the small-object optimization inside the standard library’s std::function implementation, so nearly every command allocated on the heap. Replacing the std::function queue with a small hand-written command struct hierarchy (no virtual dispatch even, just a tagged union plus a switch, since the command set was closed and known at compile time) removed the allocations entirely and the profiler trace flattened out. The API became slightly more verbose — no more writing a lambda inline at the call site — but for a hot path running every frame for every entity, that was an easy trade. The general takeaway I keep from that experience: std::function’s flexibility is not free, and “hot loop running thousands of times per second” is exactly the condition under which its hidden allocation cost stops being a rounding error and starts being a profiler-visible line item.
When Is This Pattern Overkill?
Not every callback needs to become a Command subclass, and reaching for the full pattern reflexively is its own kind of mistake — it adds a class (or several), a virtual interface, and often a smart-pointer-managed collection, all of which cost you readability and a small amount of runtime overhead. The pattern earns its complexity when at least one of these is true:
- You need more than one level of undo. A single “are you sure you want to discard changes?” confirmation dialog does not need a Command object; a document editor with a persistent undo stack does.
- The same action needs to be triggered from genuinely independent call sites (menu, toolbar, shortcut, script, macro) and you want a single source of truth for what that action does.
- You need to record, serialize, replay, or transmit the action itself, not just its effect — game input, RPC calls, event-sourced systems, and audit logs are all cases where the command as data is the actual deliverable.
- You need transactional all-or-nothing semantics across a sequence of otherwise-independent operations.
Conversely, if you have one button that calls one function with no undo requirement, no alternate invoker, and no need to log or replay it, a Command class is pure ceremony — a lambda captured directly in the button’s click handler, or a plain function pointer, does the same job with less code and less indirection to step through in a debugger. I have seen codebases where a “Command” suffix got attached to every callback out of habit, producing dozens of one-line execute() overrides that exist purely because “that’s how we do callbacks here.” That is over-engineering, not architecture — the tell is usually that nobody can name a concrete undo, replay, or multi-invoker requirement for most of those classes; they were written because the pattern was already in the codebase, not because the problem called for it.
Common Issues and Solutions
Issue 1: Receiver Lifecycle
Symptom: Dangling reference. Cause: Command references a Receiver, but the Receiver is destroyed first.
// ❌ Incorrect usage: reference
class Command {
Receiver& receiver_; // Can become dangling
};
// ✅ Correct usage: shared_ptr
class Command {
std::shared_ptr<Receiver> receiver_;
};
This shows up in exactly the scenario the undo stack was built for: a command sits in the undo history long after the original operation ran, and if the receiver it references (a document, a widget, a network connection) was destroyed in the meantime — the user closed the tab, the dialog was dismissed — calling undo() on that stale command is a use-after-free. A raw reference or pointer has no way to express “this might no longer exist”; std::shared_ptr at least keeps the receiver alive as long as any command references it, and std::weak_ptr (checked with .lock() before use) is the right choice when you specifically want undo to become a documented no-op rather than force an extended lifetime on something the user has already closed.
Issue 2: Commands That Cannot Be Undone
Symptom: Unable to restore after Undo. Cause: State is not saved.
// ❌ Incorrect usage: no state saved
class DeleteCommand : public Command {
void undo() override {
// How to restore deleted data?
}
};
// ✅ Correct usage: save state
class DeleteCommand : public Command {
std::string deletedText_; // Save state
void execute() override {
deletedText_ = doc_.getText();
doc_.clear();
}
void undo() override {
doc_.setText(deletedText_);
}
};
The general principle here is that undo() almost never has enough information to do its job unless execute() deliberately captured it beforehand — undo is not “the opposite operation” in the abstract, it is “reconstruct exactly what was true before, using data you saved on the way in.” This is also where the memory-cost discussion from Section 2 comes back around: the amount of state execute() needs to capture for undo() to work correctly is precisely the amount of memory each command instance will hold onto for as long as it sits in the undo stack. Deciding what to capture is not a side detail — it is the central design decision of the whole undo system.
Production Patterns
Pattern 1: History Limit
class CommandManager {
public:
CommandManager(size_t maxHistory = 100) : maxHistory_(maxHistory) {}
void executeCommand(std::unique_ptr<Command> cmd) {
cmd->execute();
undoStack_.push(std::move(cmd));
// Limit history
if (undoStack_.size() > maxHistory_) {
undoStack_.pop();
}
while (!redoStack_.empty()) {
redoStack_.pop();
}
}
private:
size_t maxHistory_;
std::stack<std::unique_ptr<Command>> undoStack_;
std::stack<std::unique_ptr<Command>> redoStack_;
};
One subtlety worth flagging: std::stack (backed by std::deque by default) does not give you cheap removal from the bottom, so undoStack_.pop() above is actually popping the most recently pushed element, not the oldest one — which is backwards for a history limit that is supposed to discard the oldest entries first. In practice this bug is easy to miss because it only manifests as “undo history feels shorter/weirder than expected under heavy use,” not as a crash. A correct implementation needs a container that supports efficient removal from the opposite end from where you push — a std::deque used directly (push/pop at the back, pop_front() to evict the oldest) is the usual fix, and it is a good example of how “just add a size cap” is easy to say and slightly fiddly to get right when the underlying container’s cheap and expensive ends do not line up with the direction you need to evict.
Pattern 2: Asynchronous Command
#include <future>
class AsyncCommand : public Command {
public:
void execute() override {
future_ = std::async(std::launch::async, [this]() {
// Asynchronous task
});
}
void wait() {
if (future_.valid()) {
future_.wait();
}
}
private:
std::future<void> future_;
};
Asynchronous commands introduce a question the synchronous versions never had to answer: what does “undo” mean for a command whose execute() has not finished yet, or whose receiver is being mutated from a background thread while the invoker’s thread wants to inspect it? If the receiver is not thread-safe — and most GUI widgets and game-world objects are not — you need either to marshal the actual mutation back onto the owning thread (the common approach in Qt, via queued signal/slot connections, and in game engines, via a main-thread task queue) or to make the receiver’s mutating operations genuinely safe to call concurrently. Treating an AsyncCommand as a drop-in replacement for a synchronous one without addressing this is a reliable source of data races that only reproduce under load, which is precisely when they are hardest to debug.
Complete Example: Text Editor
#include <iostream>
#include <memory>
#include <stack>
#include <string>
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual std::string describe() const = 0;
virtual ~Command() = default;
};
class TextEditor {
public:
void insert(size_t pos, const std::string& text) {
content_.insert(pos, text);
}
void erase(size_t pos, size_t len) {
content_.erase(pos, len);
}
std::string getText(size_t pos, size_t len) const {
return content_.substr(pos, len);
}
const std::string& getContent() const { return content_; }
void print() const {
std::cout << "Content: \"" << content_ << "\"\n";
}
private:
std::string content_;
};
class InsertCommand : public Command {
public:
InsertCommand(TextEditor& editor, size_t pos, const std::string& text)
: editor_(editor), position_(pos), text_(text) {}
void execute() override {
editor_.insert(position_, text_);
}
void undo() override {
editor_.erase(position_, text_.size());
}
std::string describe() const override {
return "Insert \"" + text_ + "\" at " + std::to_string(position_);
}
private:
TextEditor& editor_;
size_t position_;
std::string text_;
};
class DeleteCommand : public Command {
public:
DeleteCommand(TextEditor& editor, size_t pos, size_t len)
: editor_(editor), position_(pos), length_(len) {}
void execute() override {
deletedText_ = editor_.getText(position_, length_);
editor_.erase(position_, length_);
}
void undo() override {
editor_.insert(position_, deletedText_);
}
std::string describe() const override {
return "Delete " + std::to_string(length_) + " chars at " + std::to_string(position_);
}
private:
TextEditor& editor_;
size_t position_;
size_t length_;
std::string deletedText_;
};
class EditorController {
public:
EditorController(TextEditor& editor) : editor_(editor) {}
void execute(std::unique_ptr<Command> cmd) {
std::cout << "Executing: " << cmd->describe() << '\n';
cmd->execute();
editor_.print();
undoStack_.push(std::move(cmd));
while (!redoStack_.empty()) {
redoStack_.pop();
}
}
void undo() {
if (undoStack_.empty()) {
std::cout << "Nothing to undo\n";
return;
}
auto cmd = std::move(undoStack_.top());
undoStack_.pop();
std::cout << "Undoing: " << cmd->describe() << '\n';
cmd->undo();
editor_.print();
redoStack_.push(std::move(cmd));
}
void redo() {
if (redoStack_.empty()) {
std::cout << "Nothing to redo\n";
return;
}
auto cmd = std::move(redoStack_.top());
redoStack_.pop();
std::cout << "Redoing: " << cmd->describe() << '\n';
cmd->execute();
editor_.print();
undoStack_.push(std::move(cmd));
}
private:
TextEditor& editor_;
std::stack<std::unique_ptr<Command>> undoStack_;
std::stack<std::unique_ptr<Command>> redoStack_;
};
int main() {
TextEditor editor;
EditorController controller(editor);
controller.execute(std::make_unique<InsertCommand>(editor, 0, "Hello"));
controller.execute(std::make_unique<InsertCommand>(editor, 5, " World"));
controller.execute(std::make_unique<DeleteCommand>(editor, 5, 6));
controller.undo();
controller.undo();
controller.redo();
}
Note that this describe() method is not decorative — a Command interface that can describe itself in human-readable terms is what lets you build a visible undo/redo history list in a UI (the kind you see in Photoshop’s History panel or most IDEs’ “Local History”), and it costs nothing beyond one extra pure virtual function. It is a small example of how the pattern, once in place, tends to make a handful of related features (undo, macro recording, a visible action log) cheap to add later, because the objectified-request structure already carries the information those features need.
When the Command pattern is worth it
The pattern’s value is not the two-method interface itself, but what that interface unlocks: an invoker that can stay ignorant of receivers, a history stack that can be rewound and replayed, and individual actions that can be composed, transmitted, or logged as first-class data instead of disappearing the moment a function returns. That value has a cost — extra classes, extra indirection, and in naive implementations, unbounded memory growth in the undo stack — so the pattern is worth reaching for when you genuinely need decoupled invokers, multi-level undo, macro composition, or transactional rollback, and worth skipping when a plain callback such as std::function<void()> would do.
FAQ
Q1: When should I use the Command Pattern?
A: Use it when you need Undo/Redo, macros, transactions, or task queues.
Q2: How is it different from the Memento Pattern?
A: Command focuses on action history, while Memento focuses on state snapshots.
Q3: What about memory usage?
A: Memory usage increases with a large history stack. Use history limits.
Q4: Can Commands be asynchronous?
A: Yes, use std::async or std::thread for asynchronous execution.
Q5: How do I manage Receiver lifecycles?
A: Use shared_ptr or ensure the Receiver outlives the Command.
Q6: Where can I learn more about the Command Pattern?
A:
- “Design Patterns” by Gang of Four
- “Head First Design Patterns” by Freeman & Freeman
- Refactoring Guru: Command Pattern Use the Command Pattern to encapsulate requests and implement Undo/Redo. Next, check out State Pattern.