C++ Command Pattern: Complete Guide | Undo, Redo, Macros & Queues

C++ Command Pattern: Complete Guide | Undo, Redo, Macros & Queues

이 글의 핵심

Command pattern in C++: invoker, receiver, history stacks, composite commands, and production tips for editors and transactional workflows.

Behavioral patterns are covered in C++ behavioral patterns #20-1 and the overview #20-2.

What is Command Pattern? why you need it

Problem Scenario: Implementing Undo

Problem: To implement Undo/Redo in a text editor, you need to record all your actions and execute them in reverse order. It is difficult to record if the work is done only through function calls.

// Bad example: only function calls
void insertText(std::string& doc, const std::string& text) {
    doc += text;
// How to Undo?
}

Solution: Command Pattern encapsulates the request into an object. Each Command has execute() and undo() and is stored in the 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

index

  1. Basic structure
  2. Undo/Redo Implementation
  3. Macro system
  4. Transaction
  5. Frequently occurring problems and solutions
  6. Production Patterns
  7. Complete example: Text editor

1. basic structure

Minimum 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. Undo/Redo implementation

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 system

Compound 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. transaction

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 (...) {
// Rollback failure is ignored
            }
        }
    }
    
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. Frequently occurring problems and solutions

Problem 1: Receiver life cycle

Symptom: Dangling reference.

Cause: Command refers to Receiver, but Receiver is destroyed first.

// ❌ Misuse: See
class Command {
Receiver& receiver_;  // Dangling possible
};

// ✅ Correct usage: shared_ptr
class Command {
    std::shared_ptr<Receiver> receiver_;
};

Problem 2: Undo impossible Command

Symptom: Cannot be restored when undoing.

Cause: The state was not saved.

// ❌ Incorrect use: stateless
class DeleteCommand : public Command {
    void undo() override {
// How to restore deleted data?
    }
};

// ✅ Correct use: Save state
class DeleteCommand : public Command {
std::string deletedText_;  // save
    
    void execute() override {
        deletedText_ = doc_.getText();
        doc_.clear();
    }
    
    void undo() override {
        doc_.setText(deletedText_);
    }
};

6. production pattern

Pattern 1: History Restrictions

class CommandManager {
public:
    CommandManager(size_t maxHistory = 100) : maxHistory_(maxHistory) {}
    
    void executeCommand(std::unique_ptr<Command> cmd) {
        cmd->execute();
        undoStack_.push(std::move(cmd));
        
// history limit
        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 operation
        });
    }
    
    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();
}

organize

conceptDescription
Command PatternEncapsulate request into object
PurposeUndo/Redo, Macro, Transaction, Queue
StructureCommand, Invoker, Receiver
AdvantagesRequest history, cancelable, combinable
Disadvantagesclass increase, memory usage
Use CaseEditor, GUI, Transactions, Task Queue

Command Pattern is a powerful pattern that objectifies requests to implement Undo/Redo and macros.


FAQ

Q1: When do I use Command Pattern?

A: Used when Undo/Redo, Macro, Transaction, and Task Queue are required.

Q2: What is the difference from Memento Pattern?

A: Command focuses on action history, Memento focuses on state snapshots.

Q3: What is the memory usage?

A: Memory increases as the history stack grows. Limit history.

Q4: What is an asynchronous Command?

A: Asynchronous execution is possible with std::async or std::thread.

Q5: What is the Receiver life cycle?

A: Manage it with shared_ptr, or ensure that the Command is destroyed before the Receiver.

Q6: What are Command Pattern learning resources?

A:

One line summary: Command Pattern allows you to objectify requests and implement Undo/Redo. Next, it would be a good idea to read State Pattern.


Good article to read together (internal link)

Here’s another article related to this topic.

  • Complete Guide to C++ Observer Pattern | Event-based architecture and signals/slots
  • Complete Guide to C++ Strategy Pattern | Algorithm encapsulation and runtime replacement

Practical tips

These are tips that can be applied right away in practice.

Debugging tips

  • If you run into a problem, check the compiler warnings first.
  • Reproduce the problem with a simple test case

Performance Tips

  • Don’t optimize without profiling
  • Set measurable indicators first

Code review tips

  • Check in advance for areas that are frequently pointed out in code reviews.
  • Follow your team’s coding conventions

Practical checklist

This is what you need to check when applying this concept in practice.

Before writing code

  • Is this technique the best way to solve the current problem?
  • Can team members understand and maintain this code?
  • Does it meet the performance requirements?

Writing code

  • Have you resolved all compiler warnings?
  • Have you considered edge cases?
  • Is error handling appropriate?

When reviewing code

  • Is the intent of the code clear?
  • Are there enough test cases?
  • Is it documented?

Use this checklist to reduce mistakes and improve code quality.


Keywords covered in this article (related search terms)

This article will be helpful if you search for C++, command, pattern, undo, redo, macro, queue, etc.


  • C++ Adapter Pattern Complete Guide | Interface conversion and compatibility
  • C++ Container Adapter |
  • C++ CRTP Complete Guide | Static polymorphism and compile-time optimization
  • C++ Decorator Pattern Complete Guide | Dynamic addition and combination of functions
  • C++ Factory Pattern Complete Guide | Object creation encapsulation and extensibility