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:
MacroCommandwraps 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
- Observer pattern — notifying the UI when commands change state
- Active object pattern — command queue as the active object’s message queue
- Factory & builder patterns — building command objects