Command Pattern
Encapsulate operations as objects for deferred execution, undo/redo, queuing, and serialization — virtual hierarchies, std::function, and std::variant approaches.
Command Patternsince C++11A behavioral design pattern that encapsulates an operation — its target object, action, and arguments — as a first-class object, decoupling the point where a call is assembled from the point where it executes.
Overview
The pattern solves a specific timing problem: you need to specify what to do in one place but when to do it somewhere else. Three roles collaborate:
- Command — holds everything needed to perform (and optionally reverse) one operation
- Receiver — the domain object that changes state
- Invoker — decides when to execute; manages queues or history
This structure enables capabilities that are difficult to achieve otherwise: deferred and queued execution, undo/redo, macro recording, and serialization for audit logs or remote replay.
The appropriate implementation depends on which capabilities you need. std::function (C++11) is zero-boilerplate for pure deferred execution. A virtual hierarchy adds open extensibility and undo state. std::variant (C++17) eliminates virtual dispatch while preserving value semantics. These are not mutually exclusive — a real application often uses all three.
Examples
Fire-and-Forget Queue with std::function
When you only need deferred execution — no undo, no serialization — std::function<void()> is sufficient. It erases the type of any callable (lambda, function pointer, bound member function) under a uniform interface, acting as a generalized functor:
// C++11
#include <functional>
#include <vector>
class CommandQueue {
std::vector<std::function<void()>> pending_;
public:
void enqueue(std::function<void()> cmd) {
pending_.push_back(std::move(cmd));
}
void flush() {
for (auto& cmd : pending_) cmd();
pending_.clear();
}
};
// Any callable fits — the invoker knows nothing about the implementation
CommandQueue q;
q.enqueue([&conn] { conn.send(packet); });
q.enqueue([&db] { db.commit(); });
q.flush();This scales naturally: instead of writing CmdSendEmail, CmdCommitDB, and CmdRefreshCache as separate concrete classes, any lambda captures the necessary state inline.
Undo/Redo with a Virtual Hierarchy
When you need reversibility, each command must carry enough state to invert itself. The key invariant: execute() saves whatever undo() will need before mutating the receiver.
// C++11
struct Command {
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
class TextBuffer {
std::string text_;
public:
void insert(std::size_t pos, std::string_view s) { text_.insert(pos, s); }
void erase (std::size_t pos, std::size_t n) { text_.erase(pos, n); }
std::string_view view() const noexcept { return text_; }
};
struct InsertCmd final : Command {
TextBuffer& buf;
std::size_t pos;
std::string text;
InsertCmd(TextBuffer& b, std::size_t p, std::string t)
: buf{b}, pos{p}, text{std::move(t)} {}
void execute() override { buf.insert(pos, text); }
void undo() override { buf.erase(pos, text.size()); }
};
struct EraseCmd final : Command {
TextBuffer& buf;
std::size_t pos, len;
std::string saved; // populated during execute(), used by undo()
EraseCmd(TextBuffer& b, std::size_t p, std::size_t n)
: buf{b}, pos{p}, len{n} {}
void execute() override {
saved = std::string{buf.view().substr(pos, len)};
buf.erase(pos, len);
}
void undo() override { buf.insert(pos, saved); }
};Commands hold references to receivers, not copies. The receiver must outlive the command objects that reference it — typically guaranteed by the invoker's ownership structure.
History Stack
The invoker for undo/redo has one critical rule: any new execute() invalidates the redo stack. Failing to clear undone_ leaves stale commands that reference overwritten state.
// C++11
class History {
std::vector<std::unique_ptr<Command>> done_;
std::vector<std::unique_ptr<Command>> undone_;
public:
void execute(std::unique_ptr<Command> cmd) {
cmd->execute();
done_.push_back(std::move(cmd));
undone_.clear(); // branching edit: redo history is invalidated
}
void undo() {
if (done_.empty()) return;
auto cmd = std::move(done_.back());
done_.pop_back();
cmd->undo();
undone_.push_back(std::move(cmd));
}
void redo() {
if (undone_.empty()) return;
auto cmd = std::move(undone_.back());
undone_.pop_back();
cmd->execute();
done_.push_back(std::move(cmd));
}
bool can_undo() const noexcept { return !done_.empty(); }
bool can_redo() const noexcept { return !undone_.empty(); }
};
TextBuffer buf;
History hist;
hist.execute(std::make_unique<InsertCmd>(buf, 0, "Hello"));
hist.execute(std::make_unique<InsertCmd>(buf, 5, " World"));
// buf: "Hello World"
hist.undo(); // buf: "Hello"
hist.redo(); // buf: "Hello World"To cap memory use, replace std::vector with std::deque and evict from the front when done_.size() > max_depth. std::list is also appropriate when iteration order is the priority over random access.
Macro Command (Composite)
Group multiple commands into one undoable unit. Undo must reverse steps in reverse execution order — if step B depends on the outcome of step A, undoing A before B corrupts state:
// C++11
struct MacroCmd final : Command {
std::vector<std::unique_ptr<Command>> steps;
void execute() override {
for (auto& cmd : steps) cmd->execute();
}
void undo() override {
for (auto it = steps.rbegin(); it != steps.rend(); ++it)
(*it)->undo();
}
void add(std::unique_ptr<Command> c) { steps.push_back(std::move(c)); }
};
auto macro = std::make_unique<MacroCmd>();
macro->add(std::make_unique<InsertCmd>(buf, 0, "# Title\n"));
macro->add(std::make_unique<InsertCmd>(buf, 8, "Body.\n"));
hist.execute(std::move(macro));
hist.undo(); // both inserts reversed as one atomic stepValue-Type Commands with std::variant (C++17)
When heap allocation and virtual dispatch are unacceptable — real-time audio, embedded systems, hot paths — represent commands as a discriminated union. The trade-off: this is a closed set; adding a command type requires touching every visitor.
// C++17
#include <variant>
struct InsertCmd { std::size_t pos; std::string text; };
struct EraseCmd { std::size_t pos; std::size_t len; std::string saved; };
struct ReplaceCmd { std::size_t pos; std::string old_text; std::string new_text; };
using Cmd = std::variant<InsertCmd, EraseCmd, ReplaceCmd>;
// C++17: explicit deduction guide required; C++20 aggregates deduce without it
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>; // C++17 only
void do_execute(TextBuffer& buf, Cmd& cmd) {
std::visit(overloaded{
[&](InsertCmd& c) { buf.insert(c.pos, c.text); },
[&](EraseCmd& c) {
c.saved = std::string{buf.view().substr(c.pos, c.len)};
buf.erase(c.pos, c.len);
},
[&](ReplaceCmd& c) {
c.old_text = std::string{buf.view().substr(c.pos, c.new_text.size())};
buf.erase(c.pos, c.old_text.size());
buf.insert(c.pos, c.new_text);
},
}, cmd);
}
void do_undo(TextBuffer& buf, Cmd& cmd) {
std::visit(overloaded{
[&](InsertCmd& c) { buf.erase(c.pos, c.text.size()); },
[&](EraseCmd& c) { buf.insert(c.pos, c.saved); },
[&](ReplaceCmd& c) {
buf.erase(c.pos, c.new_text.size());
buf.insert(c.pos, c.old_text);
},
}, cmd);
}
// std::vector<Cmd> is a single contiguous allocation — no per-command heap
std::vector<Cmd> history;
void execute_cmd(TextBuffer& buf, Cmd cmd) {
do_execute(buf, cmd);
history.push_back(std::move(cmd));
}Thread-Safe Command Queue
Producer threads push; a single consumer serializes execution. The mutex scope must not include the command invocation — holding the lock while the command runs blocks producers for the full duration of the operation:
// C++17 (CTAD on lock_guard/unique_lock)
#include <queue>
#include <mutex>
#include <condition_variable>
class ConcurrentQueue {
std::queue<std::function<void()>> q_;
std::mutex mu_;
std::condition_variable cv_;
bool stop_ = false;
public:
void push(std::function<void()> cmd) {
{ std::lock_guard lock{mu_}; q_.push(std::move(cmd)); }
cv_.notify_one();
}
void run_until_stopped() {
while (true) {
std::unique_lock lock{mu_};
cv_.wait(lock, [&]{ return !q_.empty() || stop_; });
if (stop_ && q_.empty()) return;
auto cmd = std::move(q_.front());
q_.pop();
lock.unlock(); // release before executing
cmd();
}
}
void stop() {
{ std::lock_guard lock{mu_}; stop_ = true; }
cv_.notify_all();
}
};For multiple consumers, commands must be re-entrant or carry their own synchronization.
Best Practices
Match implementation to required capabilities. std::function for fire-and-forget. Virtual hierarchy when the command set is open or undo state is complex. std::variant when the set is closed and allocation matters.
Capture undo state in execute(), not in the constructor. Commands may be queued; the receiver's state at queue time can differ from its state at execution time. EraseCmd above saves the erased bytes inside execute() for exactly this reason.
Clear the redo stack on new execute. This is the most common correctness bug in history implementations. Stale redo commands reference state that has since been overwritten.
Bound history depth with std::deque. std::vector-based history grows without limit. Switch to std::deque and pop from the front at a configured maximum depth.
Release the lock before executing in a command queue. Holding the queue's mutex during command execution serializes producers for no benefit and risks priority inversion.
Common Pitfalls
Dangling receiver references. Commands hold raw references or pointers to receivers. If the receiver is destroyed before undo fires — common with document-close scenarios — the command dereferences a dead object. Use std::shared_ptr/std::weak_ptr when the receiver's lifetime is uncertain.
Non-invertible operations. Sending a network packet, writing to an append-only log, or generating a UUID have no meaningful inverse. Either mark such commands as non-undoable and skip undo, or design the receiver with explicit compensating operations (send a cancellation packet, append a tombstone).
Wrong undo order in macro commands. Iterating steps forward during undo instead of backward produces incorrect results whenever a later step's precondition depends on an earlier step's effect.
std::function overhead for high-frequency trivial callables. std::function type-erases with a potential heap allocation when the stored callable exceeds the small-buffer optimization threshold (typically 16–32 bytes). In tight loops, prefer std::variant of concrete types or bare function pointers.
Shared mutable state between commands. If two enqueued commands both capture [&counter] by reference, executing them concurrently causes a data race. Commands should either own their data or synchronize externally.
Command Queues — Deferring and Batching Execution
Deferred execution is the core value proposition of the Command pattern. Instead of calling a function immediately, you package it as an object and place it in a queue; the queue decides when (and on which thread) to drain. This unlocks frame-rate-independent game loops, animation scheduling, and UI thread serialization.
A minimal batching queue that collects commands across a frame and drains them in one pass keeps execution predictable and cache-friendly:
// C++11
#include <deque>
#include <functional>
#include <mutex>
class BatchQueue {
std::deque<std::function<void()>> pending_;
std::mutex mu_;
public:
// Thread-safe: multiple producers may enqueue concurrently
void enqueue(std::function<void()> cmd) {
std::lock_guard<std::mutex> lk{mu_};
pending_.push_back(std::move(cmd));
}
// Single-consumer drain: swap under lock, execute outside lock
void flush() {
std::deque<std::function<void()>> local;
{
std::lock_guard<std::mutex> lk{mu_};
local.swap(pending_);
}
for (auto& cmd : local) cmd();
}
std::size_t size() const {
std::lock_guard<std::mutex> lk{mu_};
return pending_.size();
}
};std::deque is preferred over std::vector here because prepending and popping from the front are O(1); it also avoids the single large reallocation that std::vector incurs when the queue grows beyond its reserved capacity. The swap-under-lock idiom keeps the critical section minimal — the actual command execution happens outside the lock so producers are never blocked by slow command bodies.
Composite Commands — Grouping into Macro Commands
A macro command is the Composite pattern applied to Command. It holds a sequence of sub-commands and presents the same interface as a single command — callers cannot tell whether they are executing one step or fifty. This is the right abstraction for "format paragraph" (select all + set font + set indent) or "rename symbol" in an IDE (rename declaration + update all references).
The virtual hierarchy from the Undo/Redo section extends naturally:
// C++11
struct TransactionCmd final : Command {
std::vector<std::unique_ptr<Command>> steps_;
bool executed_ = false;
void add(std::unique_ptr<Command> c) {
steps_.push_back(std::move(c));
}
void execute() override {
for (auto& cmd : steps_) cmd->execute();
executed_ = true;
}
// Reverse order is critical: later steps may depend on earlier ones
void undo() override {
if (!executed_) return;
for (auto it = steps_.rbegin(); it != steps_.rend(); ++it)
(*it)->undo();
executed_ = false;
}
bool empty() const noexcept { return steps_.empty(); }
};
// Building a "bold selection" macro programmatically
auto bold = std::make_unique<TransactionCmd>();
bold->add(std::make_unique<InsertCmd>(buf, sel_start, "<b>"));
bold->add(std::make_unique<InsertCmd>(buf, sel_end + 3, "</b>"));
hist.execute(std::move(bold));
// One undo() call removes both tags atomically
hist.undo();Nesting is unlimited — a TransactionCmd may itself contain other TransactionCmd objects, modeling hierarchical undo groups (word-level inside sentence-level inside paragraph-level).
Serializable Commands — Commands as Data
Serialization turns a command into a plain data transfer object (DTO) that can be persisted, logged, or sent over a network. This is essential for collaborative editing, audit trails, and replay debugging — you store the intent, not the in-memory object graph.
The pattern requires separating command identity (a type tag and parameters) from command behavior (the execute/undo logic). A compact approach maps an integer type-tag to a factory function:
// C++17
#include <cstdint>
#include <string>
#include <unordered_map>
#include <functional>
#include <memory>
// Flat, serialization-friendly POD describing what happened
struct CommandDTO {
uint32_t type; // registered type tag
uint64_t pos; // generic position argument
std::string payload; // generic string payload
};
// Serialize to bytes (e.g. for a network protocol or audit log)
// Real code would use protobuf/flatbuffers; this illustrates the concept
std::string serialize(const CommandDTO& dto) {
// type(4) + pos(8) + len(4) + payload
std::string out;
out.resize(16 + dto.payload.size());
std::memcpy(out.data() + 0, &dto.type, 4);
std::memcpy(out.data() + 4, &dto.pos, 8);
uint32_t len = static_cast<uint32_t>(dto.payload.size());
std::memcpy(out.data() + 12, &len, 4);
std::memcpy(out.data() + 16, dto.payload.data(), len);
return out;
}
// Registry maps type tags to Command factories
using Factory = std::function<std::unique_ptr<Command>(TextBuffer&, const CommandDTO&)>;
std::unordered_map<uint32_t, Factory> registry;
constexpr uint32_t CMD_INSERT = 1;
constexpr uint32_t CMD_ERASE = 2;
void register_defaults() {
registry[CMD_INSERT] = [](TextBuffer& buf, const CommandDTO& dto) {
return std::make_unique<InsertCmd>(buf, dto.pos, dto.payload);
};
registry[CMD_ERASE] = [](TextBuffer& buf, const CommandDTO& dto) {
return std::make_unique<EraseCmd>(buf, dto.pos, dto.payload.size());
};
}
std::unique_ptr<Command> deserialize(TextBuffer& buf, const CommandDTO& dto) {
auto it = registry.find(dto.type);
if (it == registry.end()) throw std::invalid_argument{"unknown command type"};
return it->second(buf, dto);
}Versioning is the hard problem: once a CommandDTO is persisted or replicated, its field layout is a public contract. Adding a field requires a migration strategy (default values, schema version number). Keep DTOs as lean as possible and evolve them deliberately.
Lambda-Based Commands — Lightweight std::function Objects
std::function<void()> is the zero-ceremony command object. When you do not need undo, serialization, or an extensible type hierarchy, a capturing lambda stored in std::function delivers the same decoupling as a full virtual class hierarchy in three lines. It is the idiomatic C++11 answer to "I just need a deferred callback."
// C++11
#include <functional>
#include <vector>
#include <string>
// A scheduler that runs commands at named lifecycle hooks
class Scheduler {
std::vector<std::function<void()>> on_start_;
std::vector<std::function<void()>> on_shutdown_;
public:
void on_start (std::function<void()> f) { on_start_.push_back(std::move(f)); }
void on_shutdown(std::function<void()> f) { on_shutdown_.push_back(std::move(f)); }
void start() {
for (auto& f : on_start_) f();
}
void shutdown() {
// Shutdown in reverse registration order
for (auto it = on_shutdown_.rbegin(); it != on_shutdown_.rend(); ++it)
(*it)();
}
};
// Registrations read naturally — no concrete command class needed
Scheduler sched;
sched.on_start ([&db] { db.connect("localhost", 5432); });
sched.on_start ([&cache] { cache.warm_up(); });
sched.on_shutdown([&cache] { cache.flush(); });
sched.on_shutdown([&db] { db.disconnect(); });The overhead to watch for is std::function's small-buffer optimization (SBO): most implementations store callables up to ~16–32 bytes inline; larger captures (e.g., a std::string member plus a pointer) spill to the heap. If a hot path enqueues thousands of commands per frame, profile the allocation pressure and consider a fixed-size SBO allocator or the std::variant-of-concrete-types approach shown above.
// C++14 — generic lambdas let you build templated command wrappers without
// naming the callable type
template<typename F>
auto make_command(F&& f) -> std::function<void()> {
return std::function<void()>{std::forward<F>(f)};
}
auto cmd = make_command([x = 42]() mutable { process(x++); });For undo support without a full virtual hierarchy, pair two std::function<void()> objects — one for execute, one for undo — in a small aggregate and push that into the history stack instead of a unique_ptr<Command>.
See Also
- RAII — command objects that acquire resources in
execute()should release them in destructors, not inundo() - Type Erasure — the technique behind
std::function; understanding it explains its overhead - Policy-Based Design — an alternative to virtual dispatch when command behavior varies at compile time