본문으로 건너뛰기 C++ Command Pattern: Complete Guide | Undo· Redo

C++ Command Pattern: Complete Guide | Undo· Redo

C++ Command Pattern: Complete Guide | Undo· Redo

이 글의 핵심

Command pattern in C++: encapsulate requests as objects for undo/redo, macros, transactions, and async work—editor and banking-style examples with SEO keywords.

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:

  • “Design Patterns” by Gang of Four
  • “Head First Design Patterns” by Freeman & Freeman
  • Refactoring Guru: Command Pattern 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.

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++, Command Pattern, Undo, Redo, Macro, Queue, Design Patterns 등으로 검색하시면 이 글이 도움이 됩니다.