C++ Command Pattern Complete Guide | Undo, Redo, and Macro Systems
이 글의 핵심
Command pattern: encapsulate actions as objects for undo/redo, macros, job queues, and transactional edits.
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
cmd <|-- insert
cmd <|-- delete
insert --> receiver
delete --> receiver
Table of Contents
- Basic Structure
- Implementing Undo/Redo
- Macro Systems
- Transactions
- Common Issues and Solutions
- Production Patterns
- Complete Example: Text Editor
1. 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
}
2. 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"
}
3. 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
}
4. 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";
}
}
5. 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_;
};
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_);
}
};
6. 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_;
};
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_;
};
7. 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();
}
Summary
| Concept | Description |
|---|---|
| Command Pattern | Encapsulates requests as objects |
| Purpose | Undo/Redo, macros, transactions, queues |
| Structure | Command, Invoker, Receiver |
| Advantages | Record requests, enable undo, combine commands |
| Disadvantages | Increased class count, memory usage |
| Use Cases | Editors, GUI, transactions, task queues |
The Command Pattern is a powerful design pattern for objectifying requests and implementing Undo/Redo and macros.
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
One-line summary: Use the Command Pattern to encapsulate requests and implement Undo/Redo. Next, check out State Pattern.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Observer Pattern 완벽 가이드 | 이벤트 기반 아키텍처와 신호/슬롯
- C++ Strategy Pattern 완벽 가이드 | 알고리즘 캡슐화와 런타임 교체
이 글에서 다루는 키워드 (관련 검색어)
C++, command, pattern, undo, redo, macro, queue 등으로 검색하시면 이 글이 도움이 됩니다.