Operations as objects

The command pattern encapsulates an operation as an object. Instead of calling a function directly, the caller creates a command object and passes it to an executor. The executor decides when (and whether) to run it.

This enables:

  • Undo/redo — each command knows how to reverse itself
  • Macro recording — collect commands for replay
  • Deferred execution — queue commands for later
  • Logging — log commands as they execute
  • Transaction rollback — execute a list; on failure, reverse all

Basic command interface

1class ICommand {
2public:
3    virtual void execute() = 0;
4    virtual void undo()    = 0;
5    virtual ~ICommand()    = default;
6};

A concrete command captures its receiver (the object it operates on) and any parameters in its constructor:

 1class SetTemperatureCommand : public ICommand {
 2    Thermostat& thermostat_;
 3    float       newTemp_;
 4    float       prevTemp_;   // saved for undo
 5
 6public:
 7    SetTemperatureCommand(Thermostat& t, float temp)
 8        : thermostat_(t), newTemp_(temp) {}
 9
10    void execute() override {
11        prevTemp_ = thermostat_.getSetPoint();
12        thermostat_.setSetPoint(newTemp_);
13    }
14
15    void undo() override {
16        thermostat_.setSetPoint(prevTemp_);
17    }
18};

Usage:

1auto cmd = std::make_unique<SetTemperatureCommand>(thermostat, 23.0f);
2cmd->execute();

Undo/redo stack

 1class CommandHistory {
 2    std::vector<std::unique_ptr<ICommand>> done_;
 3    std::vector<std::unique_ptr<ICommand>> undone_;
 4
 5public:
 6    void execute(std::unique_ptr<ICommand> cmd) {
 7        cmd->execute();
 8        done_.push_back(std::move(cmd));
 9        undone_.clear();   // new action invalidates redo stack
10    }
11
12    bool canUndo() const { return !done_.empty(); }
13    bool canRedo() const { return !undone_.empty(); }
14
15    void undo() {
16        if (done_.empty()) return;
17        auto cmd = std::move(done_.back());
18        done_.pop_back();
19        cmd->undo();
20        undone_.push_back(std::move(cmd));
21    }
22
23    void redo() {
24        if (undone_.empty()) return;
25        auto cmd = std::move(undone_.back());
26        undone_.pop_back();
27        cmd->execute();
28        done_.push_back(std::move(cmd));
29    }
30};

Usage:

1CommandHistory history;
2
3history.execute(std::make_unique<SetTemperatureCommand>(t, 23.0f));
4history.execute(std::make_unique<SetTemperatureCommand>(t, 25.0f));
5// thermostat is at 25°C
6
7history.undo();   // back to 23°C
8history.undo();   // back to original
9history.redo();   // forward to 23°C

Qt integration — QUndoCommand

Qt provides a ready-made command pattern in QUndoStack / QUndoCommand. It adds: description strings (shown in Edit → Undo menu), command merging (group rapid consecutive changes), and automatic undo/redo menu action binding.

 1#include <QUndoCommand>
 2#include <QUndoStack>
 3
 4class SetThermostatCommand : public QUndoCommand {
 5    Thermostat* thermostat_;
 6    float       newTemp_;
 7    float       prevTemp_;
 8
 9public:
10    SetThermostatCommand(Thermostat* t, float temp, QUndoCommand* parent = nullptr)
11        : QUndoCommand(QString("Set temperature to %1°C").arg(temp), parent)
12        , thermostat_(t), newTemp_(temp) {}
13
14    void redo() override {
15        prevTemp_ = thermostat_->setPoint();
16        thermostat_->setSetPoint(newTemp_);
17    }
18
19    void undo() override {
20        thermostat_->setSetPoint(prevTemp_);
21    }
22
23    // Merge consecutive set-temperature commands into one undo step
24    bool mergeWith(const QUndoCommand* other) override {
25        if (other->id() != id()) return false;
26        newTemp_ = static_cast<const SetThermostatCommand*>(other)->newTemp_;
27        return true;
28    }
29
30    int id() const override { return 42; }   // unique ID for merging
31};

Wiring up to the UI:

 1// In the widget
 2QUndoStack* undoStack = new QUndoStack(this);
 3
 4// Connect to menu actions — Qt handles text and enable/disable
 5QAction* undoAction = undoStack->createUndoAction(this, tr("&Undo"));
 6QAction* redoAction = undoStack->createRedoAction(this, tr("&Redo"));
 7undoAction->setShortcut(QKeySequence::Undo);
 8redoAction->setShortcut(QKeySequence::Redo);
 9
10// Execute via stack
11void MainWindow::onSliderChanged(int value) {
12    float temp = value / 10.0f;
13    undoStack->push(new SetThermostatCommand(thermostat_, temp));
14}

The undo menu shows “Undo Set temperature to 23.0°C” — the command’s description string. Rapid slider moves are merged into one undo step.


Deferred execution queue

Commands make it straightforward to defer operations for later execution:

 1class CommandQueue {
 2    std::queue<std::unique_ptr<ICommand>> queue_;
 3    std::mutex mtx_;
 4
 5public:
 6    void enqueue(std::unique_ptr<ICommand> cmd) {
 7        std::lock_guard lk(mtx_);
 8        queue_.push(std::move(cmd));
 9    }
10
11    void executeAll() {
12        std::queue<std::unique_ptr<ICommand>> local;
13        {
14            std::lock_guard lk(mtx_);
15            local.swap(queue_);
16        }
17        while (!local.empty()) {
18            local.front()->execute();
19            local.pop();
20        }
21    }
22};

Usage: UI thread enqueues commands; device thread drains the queue in its main loop. No direct cross-thread calls to device state.

 1// UI thread — enqueue safely
 2queue.enqueue(std::make_unique<SetTemperatureCommand>(device, 22.5f));
 3
 4// Device thread — execute in its own context
 5void deviceLoop() {
 6    while (running) {
 7        queue.executeAll();
 8        device.update();
 9        HAL_Delay(10);
10    }
11}

Macro recording

Record a sequence of commands for replay:

 1class MacroCommand : public ICommand {
 2    std::vector<std::unique_ptr<ICommand>> steps_;
 3public:
 4    void add(std::unique_ptr<ICommand> step) {
 5        steps_.push_back(std::move(step));
 6    }
 7
 8    void execute() override {
 9        for (auto& s : steps_) s->execute();
10    }
11
12    void undo() override {
13        for (auto it = steps_.rbegin(); it != steps_.rend(); ++it)
14            (*it)->undo();
15    }
16};
17
18// Recording session
19auto macro = std::make_unique<MacroCommand>();
20macro->add(std::make_unique<SetTemperatureCommand>(t, 20.0f));
21macro->add(std::make_unique<SetFanSpeedCommand>(fan, 50));
22macro->add(std::make_unique<EnableAlarmCommand>(alarm, true));
23
24// Execute or add to history as one unit
25history.execute(std::move(macro));
26history.undo();   // reverses all three steps

Lambda-based command (no class hierarchy)

For simple cases with no undo, a lambda pair is enough:

 1struct Command {
 2    std::function<void()> doIt;
 3    std::function<void()> undoIt;
 4};
 5
 6class SimpleHistory {
 7    std::vector<Command> history_;
 8    size_t current_ = 0;
 9
10public:
11    void execute(Command cmd) {
12        history_.resize(current_);   // clear redo
13        cmd.doIt();
14        history_.push_back(std::move(cmd));
15        ++current_;
16    }
17
18    void undo() {
19        if (current_ == 0) return;
20        history_[--current_].undoIt();
21    }
22
23    void redo() {
24        if (current_ >= history_.size()) return;
25        history_[current_++].doIt();
26    }
27};
28
29// Usage — no class to write
30float prevTemp = thermostat.setPoint();
31history.execute({
32    [&] { thermostat.setSetPoint(23.0f); },
33    [&, prevTemp] { thermostat.setSetPoint(prevTemp); }
34});

Useful when the number of command types is small and the undo logic is trivial.


Summary

  • Command pattern: encapsulate an operation + its inverse as an object
  • Undo/redo stack: push executed commands; pop for undo, re-push for redo; clear redo on new execute
  • Qt: QUndoCommand + QUndoStack — description strings, merge, menu action binding
  • Deferred queue: UI thread enqueues, device thread drains — safe cross-thread sequencing
  • Macro: MacroCommand wraps a list of commands — execute and undo as one atomic step
  • Lambda variant: {doIt, undoIt} pair for simple cases with no class hierarchy

What’s next