C++ Command Pattern Complete Guide | Undo, Redo, and Macro Systems

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

  1. Basic Structure
  2. Implementing Undo/Redo
  3. Macro Systems
  4. Transactions
  5. Common Issues and Solutions
  6. Production Patterns
  7. 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

ConceptDescription
Command PatternEncapsulates requests as objects
PurposeUndo/Redo, macros, transactions, queues
StructureCommand, Invoker, Receiver
AdvantagesRecord requests, enable undo, combine commands
DisadvantagesIncreased class count, memory usage
Use CasesEditors, 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:

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 등으로 검색하시면 이 글이 도움이 됩니다.