Skip to content
C++
Idiom
since C++11
Basic

Memento Pattern

Capture and restore object state in C++ without exposing internals — snapshots, undo/redo stacks, opaque mementos, and delta encoding.

Memento Patternsince C++11

An originator object produces a snapshot of its private state (the memento) that a caretaker can store and hand back later to restore the originator — without the caretaker ever inspecting the state directly.

Overview

Three roles define the pattern:

  • Originator — owns mutable state; knows how to snapshot itself (save()) and restore from a snapshot (restore()).
  • Memento — an immutable value carrying the saved state; the caretaker stores it but must not interpret it.
  • Caretaker — manages a collection of mementos (an undo stack, a checkpoint list) and decides when to save and restore.

The core design tension is encapsulation: the caretaker must hold the memento but must not be able to read or modify the originator's state through it. C++ resolves this with private nested types or opaque type aliases, ensuring the memento's outward interface toward the caretaker is intentionally narrow.

Use Memento when you need reversible operations — undo/redo, game saves, transaction rollback — and snapshots must not leak implementation details beyond the originator. When the full state is expensive to copy and changes are local, prefer Command-based undo that captures only the delta rather than snapshotting the entire object.

Basic Value-Semantics Memento

When state is composed of value types (std::string, std::vector, arithmetic), the memento is a plain copyable struct. C++11 move semantics make returning and storing snapshots efficient without manual heap allocation.

cpp
#include <string>
#include <string_view>  // C++17
#include <algorithm>

class TextEditor {
    std::string text_;
    std::size_t cursor_ = 0;

public:
    struct Snapshot {
        std::string text;
        std::size_t cursor;
    };

    [[nodiscard]] Snapshot save() const {   // [[nodiscard]]: C++17
        return {text_, cursor_};            // NRVO; no copy in practice
    }

    void restore(const Snapshot& s) {
        text_   = s.text;
        cursor_ = s.cursor;
    }

    void insert(std::string_view s) {   // std::string_view: C++17
        text_.insert(cursor_, s);
        cursor_ += s.size();
    }

    void erase(std::size_t n) {
        n = std::min(n, cursor_);
        text_.erase(cursor_ - n, n);
        cursor_ -= n;
    }

    [[nodiscard]] std::string_view text()   const { return text_;   }
    [[nodiscard]] std::size_t      cursor() const { return cursor_; }
};

[[nodiscard]] on save() (C++17) ensures a discarded snapshot produces a compile-time warning — silently losing a snapshot is almost always a bug.

Undo/Redo Stack (Caretaker)

A linear history invalidates the redo branch whenever a new action is performed — the standard behaviour in most editors.

cpp
#include <vector>
#include <cstddef>

class UndoableEditor {
    TextEditor                        editor_;
    std::vector<TextEditor::Snapshot> history_;
    std::size_t                       pos_ = 0;

    void commit() {
        // Erase the redo branch, then push current state
        history_.erase(
            history_.begin() + static_cast<std::ptrdiff_t>(pos_),
            history_.end());
        history_.push_back(editor_.save());  // C++11: push_back(T&&) moves
        ++pos_;
    }

public:
    UndoableEditor() { commit(); }  // snapshot the initial empty state

    void insert(std::string_view s) { editor_.insert(s); commit(); }
    void erase(std::size_t n)       { editor_.erase(n);  commit(); }

    bool undo() {
        if (pos_ <= 1) return false;           // nothing before initial snapshot
        editor_.restore(history_[--pos_ - 1]);
        return true;
    }

    bool redo() {
        if (pos_ >= history_.size()) return false;
        editor_.restore(history_[pos_++]);
        return true;
    }

    [[nodiscard]] std::string_view text() const { return editor_.text(); }
};

pos_ always points one past the current snapshot index. history_[pos_ - 1] is the current state; history_[pos_ - 2] is the state before the last action. This invariant makes the off-by-one logic trivially auditable.

Opaque Memento (True Encapsulation)

A public nested struct exposes field names to the caretaker. For genuinely private state, use a private nested class with the originator as its only friend.

cpp
#include <vector>
#include <algorithm>

class Game {
public:
    // Caretaker sees the type but cannot construct or read it.
    class Checkpoint {
        friend class Game;
        int level_, score_, hp_;
        std::vector<int> inventory_;

        Checkpoint(int l, int s, int h, std::vector<int> inv)
            : level_{l}, score_{s}, hp_{h},
              inventory_{std::move(inv)} {}    // C++11: move into member
    };

    [[nodiscard]] Checkpoint save() const {
        return {state_.level, state_.score, state_.hp, state_.inventory};
    }

    void load(const Checkpoint& cp) {
        state_.level     = cp.level_;
        state_.score     = cp.score_;
        state_.hp        = cp.hp_;
        state_.inventory = cp.inventory_;
    }

    void advance() { ++state_.level; state_.score += 1000; }
    void damage(int d) { state_.hp = std::max(0, state_.hp - d); }

private:
    struct State {
        int level = 1, score = 0, hp = 100;
        std::vector<int> inventory;
    } state_;
};

// Caretaker usage
Game g;
g.advance();
auto cp = g.save();   // Level 1, Score 1000, HP 100
g.damage(50);
g.advance();          // Level 2, Score 2000, HP 50
g.load(cp);           // back to Level 1, Score 1000, HP 100

The caretaker holds Checkpoint values and passes them back to Game::load(), but cannot read level_ — it is private, and only Game is a friend. This is the GoF intent: narrow the memento's outward interface to zero.

Command + Memento for Fine-Grained Undo

When full-state copies are expensive (a large document model, a scene graph), pair Command-pattern lambdas with targeted before/after captures. Each command stores only the delta, not the whole object.

cpp
#include <functional>   // std::function: C++11
#include <vector>
#include <cstddef>

struct Command {
    std::function<void()> exec;
    std::function<void()> undo;
};

class CommandHistory {
    std::vector<Command> log_;
    std::size_t          pos_ = 0;

public:
    void run(Command cmd) {
        log_.erase(log_.begin() + static_cast<std::ptrdiff_t>(pos_), log_.end());
        cmd.exec();
        log_.push_back(std::move(cmd));
        ++pos_;
    }

    void undo() { if (pos_ > 0) log_[--pos_].undo(); }
    void redo() { if (pos_ < log_.size()) log_[pos_++].exec(); }
};

// --- usage ---
int x = 0;
CommandHistory hist;

// Designated initializers require C++20; use positional init in C++17 and earlier
hist.run({ .exec = [&]{ x += 10; }, .undo = [&]{ x -= 10; } });
hist.run({ .exec = [&]{ x *= 2;  }, .undo = [&]{ x /= 2;  } });
// x == 20

hist.undo();  // x == 10
hist.undo();  // x ==  0
hist.redo();  // x == 10

This avoids copying the full state on every action. The trade-off: undo closures must be carefully authored — a bug in the inverse operation silently corrupts state rather than failing visibly.

Delta Mementos for Large State

When state is large but edits are local, full snapshots are wasteful. Record only what changed per transaction.

cpp
#include <unordered_map>
#include <optional>     // C++17
#include <string>

class Spreadsheet {
    std::unordered_map<std::string, double> cells_;

public:
    struct Delta {
        // nullopt means the cell was absent before the edit
        std::unordered_map<std::string, std::optional<double>> before;
    };

    // Call before modifying a cell to record its prior state into d.
    void record(const std::string& key, Delta& d) {
        if (d.before.count(key)) return;   // already recorded this key
        auto it = cells_.find(key);
        d.before[key] = (it != cells_.end())
            ? std::optional{it->second}
            : std::nullopt;
    }

    void set(const std::string& key, double value) { cells_[key] = value; }

    void revert(const Delta& d) {
        for (const auto& [key, prior] : d.before) {   // structured bindings: C++17
            if (prior) cells_[key] = *prior;
            else       cells_.erase(key);
        }
    }
};

// Usage
Spreadsheet sheet;
Delta d;
sheet.record("A1", d);
sheet.record("B2", d);
sheet.set("A1", 42.0);
sheet.set("B2", 7.5);
sheet.revert(d);   // A1 and B2 are absent again

Delta accumulates all cells touched in a transaction; revert() replays them backward. For a spreadsheet with a million cells, this is orders of magnitude cheaper than snapshotting the whole map.

Best Practices

Make mementos immutable after creation. Once save() returns, the snapshot must not be modified. Declare data members const inside the memento struct, or document and enforce the contract at the caretaker boundary.

Apply [[nodiscard]] to save(). The attribute (C++17) turns a discarded snapshot into a compile-time warning, catching the class of bug where the programmer forgets to store the result.

Cap history depth. Unbounded undo stacks leak memory proportional to state size times history length. Enforce a maximum by erasing from the front of the history vector, or use a fixed-capacity circular buffer.

Use value types throughout the originator's state. Raw pointers in the state struct require explicit deep-copy logic in save(); value types (std::vector, std::string, std::variant) copy automatically and correctly.

Version mementos for persistent save files. If snapshots are serialised to disk (save games, crash recovery), include a schema version field. Add a static from_bytes() factory that rejects unsupported versions with a clear error rather than silently deserialising garbage.

Common Pitfalls

Shallow copy of pointer-owning members. If the originator holds a std::shared_ptr<T>, copying the state struct copies the pointer — not the pointee. The memento and the live object then alias the same heap data; mutations after save() corrupt the snapshot. Use std::make_shared<T>(*ptr_) for a deep copy, or restructure state to use value types.

Forgotten redo-branch invalidation. A new action after an undo must erase the redo entries. Missing this step allows redo() to replay superseded operations, producing logically impossible state.

Non-atomic snapshots under concurrency. save() reads several fields in sequence; if another thread mutates the originator mid-read, the snapshot is torn. Either take the snapshot under a mutex, or model state as an immutable std::shared_ptr<const State> swapped atomically — then save() reduces to a single pointer copy, which is lock-free on most platforms.

Memory bloat from large-state snapshots. A 5 MB document with 300 undo steps costs 1.5 GB of live memory. Profile snapshot sizes before assuming they are cheap; switch to delta encoding or command-based undo when they are not.

Caretaker inspecting memento internals. Once a caretaker reads fields to display "undo: typed 'hello'", the encapsulation boundary has collapsed. If human-readable undo descriptions are needed, expose them via a label() method on the Command or a separate audit log — never by opening up the memento struct.

Undo/redo stack implementation

A bounded circular-buffer history gives O(1) push and automatic eviction of the oldest snapshot. This avoids the linear-time erase-from-front that std::vector requires and caps memory usage without any explicit cleanup loop.

cpp
#include <array>
#include <cstddef>
#include <optional>   // C++17

template <typename Snapshot, std::size_t Cap>
class RingHistory {
    std::array<std::optional<Snapshot>, Cap> buf_{};
    std::size_t head_ = 0;   // oldest entry index
    std::size_t size_ = 0;   // live entries (0..Cap)
    std::size_t pos_  = 0;   // current position within live entries

public:
    // Push a new snapshot; if at capacity, the oldest entry is silently dropped.
    void push(Snapshot s) {
        // Erase any redo entries above pos_ first
        for (std::size_t i = pos_; i < size_; ++i)
            buf_[(head_ + i) % Cap].reset();
        size_ = pos_;

        if (size_ == Cap) {
            // Overwrite oldest
            buf_[head_] = std::move(s);
            head_ = (head_ + 1) % Cap;
        } else {
            buf_[(head_ + size_) % Cap] = std::move(s);
            ++size_;
        }
        ++pos_;
    }

    std::optional<Snapshot> undo() {
        if (pos_ == 0) return std::nullopt;
        return buf_[(head_ + --pos_) % Cap];  // C++17 optional
    }

    std::optional<Snapshot> redo() {
        if (pos_ >= size_) return std::nullopt;
        return buf_[(head_ + pos_++) % Cap];
    }
};

Setting Cap to a compile-time constant (e.g. RingHistory<TextEditor::Snapshot, 200>) gives a hard upper bound on memory without a single heap allocation for the history container itself.

Serialization-based memento

Serializing state to bytes decouples the memento lifetime from the originator's type, enabling save-to-disk, crash recovery, and cross-process checkpointing. The originator exposes serialize() / deserialize() rather than a typed snapshot struct; the caretaker holds std::vector<std::byte> blobs that it can write to a file or send over a socket.

cpp
#include <vector>
#include <cstddef>
#include <cstring>
#include <stdexcept>

// Simple little-endian binary packing helpers (C++17)
namespace serial {
    template <typename T>
    void write(std::vector<std::byte>& buf, const T& value) {
        static_assert(std::is_trivially_copyable_v<T>);  // C++17 type trait
        const auto* src = reinterpret_cast<const std::byte*>(&value);
        buf.insert(buf.end(), src, src + sizeof(T));
    }

    template <typename T>
    T read(const std::byte* &ptr) {
        static_assert(std::is_trivially_copyable_v<T>);
        T value{};
        std::memcpy(&value, ptr, sizeof(T));
        ptr += sizeof(T);
        return value;
    }
}  // namespace serial

class GameState {
    int  level_ = 1;
    int  score_ = 0;
    int  hp_    = 100;

public:
    // Returns an opaque byte blob — version-stamped for forward compatibility
    [[nodiscard]] std::vector<std::byte> save() const {  // C++17
        std::vector<std::byte> buf;
        serial::write(buf, std::uint32_t{1});  // schema version
        serial::write(buf, level_);
        serial::write(buf, score_);
        serial::write(buf, hp_);
        return buf;
    }

    void load(const std::vector<std::byte>& blob) {
        const std::byte* p = blob.data();
        auto ver = serial::read<std::uint32_t>(p);
        if (ver != 1) throw std::runtime_error{"unsupported save version"};
        level_ = serial::read<int>(p);
        score_ = serial::read<int>(p);
        hp_    = serial::read<int>(p);
    }

    void advance() { ++level_; score_ += 1000; }
};

Versioning is non-negotiable for any blob that leaves the process boundary. Incrementing the schema version and adding a migration path in load() keeps old save files usable after refactors.

Memento with move semantics

Moving a large snapshot into the undo stack instead of copying it can eliminate multi-megabyte allocations on every keystroke. The originator returns a snapshot by value; C++11 move semantics transfer heap-allocated members (std::vector, std::string) in O(1) time.

cpp
#include <vector>
#include <string>
#include <utility>  // std::move: C++11

class Document {
    std::string                    title_;
    std::vector<std::string>       paragraphs_;
    std::vector<std::vector<int>>  revisions_;   // large auxiliary data

public:
    struct Snapshot {
        std::string                    title;
        std::vector<std::string>       paragraphs;
        std::vector<std::vector<int>>  revisions;
    };

    // save() returns a full deep copy — caller moves it into the history
    [[nodiscard]] Snapshot save() const {  // C++17 [[nodiscard]]
        return {title_, paragraphs_, revisions_};
    }

    // restore() accepts by value so the caller can move in a stack element
    void restore(Snapshot s) {
        title_      = std::move(s.title);       // C++11 move assignment
        paragraphs_ = std::move(s.paragraphs);
        revisions_  = std::move(s.revisions);
    }

    void append(std::string para) {
        paragraphs_.push_back(std::move(para));
    }
};

// Caretaker: move snapshots in and out of the stack
std::vector<Document::Snapshot> stack;
Document doc;
doc.append("Introduction");
stack.push_back(doc.save());        // copy into stack
doc.append("Chapter 1");
stack.push_back(doc.save());

doc.restore(std::move(stack.back())); // C++11: move out of stack, no copy
stack.pop_back();

Pass Snapshot by value to restore() — the caller decides whether to copy or move. A caretaker that moves out of the stack avoids a redundant deep copy of the snapshot data.

Command + memento for complex undo

For operations that are difficult to invert analytically, embed a pre-operation snapshot directly inside the command object. This hybrid captures only the state needed to reverse one specific operation rather than duplicating the full object on every single action.

cpp
#include <functional>
#include <memory>
#include <vector>
#include <string>

class Canvas {
public:
    struct LayerState { std::string pixels; int opacity; };

    [[nodiscard]] LayerState captureLayer(int id) const {
        return layers_.at(id);
    }
    void restoreLayer(int id, LayerState s) {
        layers_.at(id) = std::move(s);
    }
    void paint(int id, std::string_view data, int opacity) {  // C++17
        layers_.at(id) = {std::string{data}, opacity};
    }
    void addLayer()  { layers_.push_back({"", 100}); }

private:
    std::vector<LayerState> layers_;
};

struct PaintCommand {
    int         layerId;
    std::string newPixels;
    int         newOpacity;

    Canvas::LayerState before;  // memento — captured just before exec()

    void exec(Canvas& c) {
        before = c.captureLayer(layerId);         // snapshot only this layer
        c.paint(layerId, newPixels, newOpacity);
    }
    void undo(Canvas& c) {
        c.restoreLayer(layerId, std::move(before));
    }
};

// Usage
Canvas canvas;
canvas.addLayer();

std::vector<PaintCommand> history;
PaintCommand cmd{0, "circle", 80, {}};
cmd.exec(canvas);
history.push_back(std::move(cmd));

history.back().undo(canvas);   // restores only layer 0, not the whole canvas

Embedding the memento in the command keeps them in sync — the undo data is always paired with the forward operation that produced it, making accidental mismatches impossible.

Memento and thread safety

Shared originator state under concurrent access requires synchronisation around both save() and restore() to avoid torn reads and partially-applied writes. The simplest correct approach is to hold a mutex for the duration of each operation.

cpp
#include <mutex>
#include <shared_mutex>  // C++17
#include <string>

class SharedDocument {
    mutable std::shared_mutex mu_;   // C++17: shared_mutex
    std::string               text_;
    std::size_t               cursor_ = 0;

public:
    struct Snapshot { std::string text; std::size_t cursor; };

    // Multiple threads may call save() concurrently — shared (read) lock.
    [[nodiscard]] Snapshot save() const {
        std::shared_lock lock{mu_};  // C++17 CTAD; shared = multiple readers ok
        return {text_, cursor_};
    }

    // Only one thread may restore() at a time — exclusive (write) lock.
    void restore(const Snapshot& s) {
        std::unique_lock lock{mu_};  // C++11
        text_   = s.text;
        cursor_ = s.cursor;
    }

    void insert(std::string_view sv) {
        std::unique_lock lock{mu_};
        text_.insert(cursor_, sv);
        cursor_ += sv.size();
    }
};

An alternative to locking is making the internal state an immutable value type held behind std::shared_ptr<const State>. Then save() is an atomic pointer copy — lock-free on all mainstream platforms — and restore() atomically swaps in a new pointer.

cpp
#include <atomic>
#include <memory>  // std::shared_ptr, std::make_shared: C++11

struct ImmutableState {
    std::string text;
    std::size_t cursor = 0;
};

class LockFreeDocument {
    std::shared_ptr<const ImmutableState> state_{
        std::make_shared<ImmutableState>()};

public:
    using Snapshot = std::shared_ptr<const ImmutableState>;

    [[nodiscard]] Snapshot save() const { return std::atomic_load(&state_); }  // C++11

    void restore(const Snapshot& s) { std::atomic_store(&state_, s); }  // C++11

    void insert(std::string_view sv) {
        // Build new immutable state; swap atomically
        auto current = std::atomic_load(&state_);
        auto next = std::make_shared<ImmutableState>(*current);
        next->text.insert(next->cursor, sv);
        next->cursor += sv.size();
        std::atomic_store(&state_, std::move(next));
    }
};

std::atomic_load / std::atomic_store on std::shared_ptr are deprecated in C++20 in favour of std::atomic<std::shared_ptr<T>>, which provides the same lock-free semantics through a cleaner interface.

Memento vs std::variant state snapshots

When an originator can exist in several structurally-different states, std::variant is a natural fit for the snapshot type. Rather than maintaining parallel optional fields or a type-tag integer, the variant discriminates automatically and makes invalid state combinations unrepresentable.

cpp
#include <variant>   // C++17
#include <string>
#include <optional>
#include <stdexcept>

// Three structurally distinct states for a network connection
struct Disconnected {};
struct Connecting  { std::string host; std::uint16_t port; };
struct Connected   { std::string host; std::uint16_t port; int fd; };

using ConnectionState = std::variant<Disconnected, Connecting, Connected>;  // C++17

class Connection {
    ConnectionState state_{Disconnected{}};

public:
    using Snapshot = ConnectionState;

    [[nodiscard]] Snapshot save() const { return state_; }   // copy variant

    void restore(Snapshot s) { state_ = std::move(s); }

    void connect(std::string host, std::uint16_t port) {
        if (!std::holds_alternative<Disconnected>(state_))
            throw std::logic_error{"already connecting or connected"};
        state_ = Connecting{std::move(host), port};
    }

    void onConnected(int fd) {
        auto* c = std::get_if<Connecting>(&state_);
        if (!c) throw std::logic_error{"unexpected state"};
        state_ = Connected{c->host, c->port, fd};
    }

    void disconnect() { state_ = Disconnected{}; }

    // std::visit dispatches on the active alternative — C++17
    void describe() const {
        std::visit([](const auto& s) {
            using T = std::decay_t<decltype(s)>;
            if constexpr (std::is_same_v<T, Disconnected>)
                puts("disconnected");
            else if constexpr (std::is_same_v<T, Connecting>)
                printf("connecting to %s:%d\n", s.host.c_str(), s.port);
            else
                printf("connected fd=%d\n", s.fd);
        }, state_);
    }
};

// Caretaker
Connection conn;
conn.connect("db.example.com", 5432);
auto snap = conn.save();         // Snapshot holds Connecting{...}
conn.onConnected(7);
conn.disconnect();
conn.restore(snap);              // back to Connecting — variant enforces legality

The key advantage over a plain struct memento is exhaustive handling: std::visit forces every alternative to be addressed, so adding a new state to the variant produces a compile error at every unhandled visit site rather than a silent runtime bug.

See Also

  • Command Pattern — pairs with Memento for fine-grained, delta-based undo/redo
  • RAII — related discipline of tying resource management to object lifetime
  • Copy-and-Swap — value-semantics technique useful in exception-safe restore() implementations