State Pattern


  • Description: Object alters behavior when its internal state changes — looks like it changed class; encapsulates state-dependent code as separate objects with transitions wired between them.
  • My Notion Note ID: K2C-2-19
  • Created: 2026-05-22
  • Updated: 2026-05-22
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Intent

  • Replace large switch (currentState) blocks in every method with one object per state.
  • Each state encapsulates: behavior for that state + transitions to other states.
  • Context delegates → current state object. State swap = behavior swap.
  • Canonical examples: TCP connection (Listen, Established, Closed), text editor modes (vim Normal/Insert/Visual), document workflow (Draft/Review/Published), parser states.

2. Structure

  • Context — holds a pointer/handle to the current State. Forwards requests to it. Exposes a transitionTo(newState) operation.
  • State (interface) — declares the operations relevant to all states.
  • ConcreteStateA, ConcreteStateB, ... — implement behavior + decide transitions (often calling context.transitionTo(...)).

Transition placement — two schools:

  • State decides — concrete state calls context.setState(new ConcreteStateX{}). State knows its successors.
  • Context decides — state returns a token / enum; context owns the transition table. Cleaner separation but more glue.

3. C++ Example

Classic OOP form — TCP-style connection:

#include <memory>
#include <iostream>

class Connection;

struct State {
    virtual ~State() = default;
    virtual void open(Connection&)  { /* default: ignore */ }
    virtual void close(Connection&) { /* default: ignore */ }
    virtual void send(Connection&, std::string_view) { /* default: ignore */ }
    virtual const char* name() const = 0;
};

class Connection {
    std::unique_ptr<State> state_;
public:
    explicit Connection(std::unique_ptr<State> s) : state_(std::move(s)) {}
    void transitionTo(std::unique_ptr<State> s) {
        std::cout << "transition: " << state_->name() << " -> " << s->name() << "\n";
        state_ = std::move(s);
    }
    void open()                      { state_->open(*this); }
    void close()                     { state_->close(*this); }
    void send(std::string_view msg)  { state_->send(*this, msg); }
};

struct Closed      : State { void open(Connection&) override;  const char* name() const override { return "Closed"; } };
struct Established : State {
    void close(Connection& c) override { c.transitionTo(std::make_unique<Closed>()); }
    void send(Connection&, std::string_view m) override { std::cout << "send: " << m << "\n"; }
    const char* name() const override { return "Established"; }
};
void Closed::open(Connection& c) { c.transitionTo(std::make_unique<Established>()); }

int main() {
    Connection c{std::make_unique<Closed>()};
    c.send("ignored");            // Closed.send → no-op
    c.open();                     // Closed → Established
    c.send("hello");              // prints "send: hello"
    c.close();                    // Established → Closed
}

4. Modern Variant with std::variant

std::variant<StateA, StateB, ...> + std::visit removes heap allocation + virtual dispatch:

#include <variant>
#include <iostream>
#include <string_view>

struct Closed {};
struct Established {};
using StateV = std::variant<Closed, Established>;

class Connection {
    StateV state_ = Closed{};
public:
    void open() {
        state_ = std::visit([](auto& s) -> StateV {
            using T = std::decay_t<decltype(s)>;
            if constexpr (std::is_same_v<T, Closed>) return Established{};
            else                                     return s;
        }, state_);
    }
    void close() {
        state_ = std::visit([](auto& s) -> StateV {
            using T = std::decay_t<decltype(s)>;
            if constexpr (std::is_same_v<T, Established>) return Closed{};
            else                                          return s;
        }, state_);
    }
    void send(std::string_view m) {
        std::visit([&](auto& s) {
            using T = std::decay_t<decltype(s)>;
            if constexpr (std::is_same_v<T, Established>) std::cout << "send: " << m << "\n";
        }, state_);
    }
};

Trade-off: closed set of states (all known at compile time) → exhaustive transitions, no allocation, fits in a small stack object. Add a new state = recompile every visitor.


5. When to Use / When Not To

Use when:

  • Object behavior depends on state and switch/if chains repeat across methods.
  • State count is small + bounded; transitions are explicit.
  • Protocols, finite-state machines, parsers, UI modes.

Avoid when:

  • Only one or two trivial branches → plain if is clearer.
  • States need to be added by clients at runtime → consider Strategy instead.
  • Many orthogonal state axes → flag soup; better modeled as multiple FSMs or a state-chart library (e.g. Boost.SML, Boost.MSM).

6. Variants and Pitfalls

  • Stateless concrete states — share a single instance per state class (Flyweight-style). Saves allocations.
  • Flat vs hierarchical — flat FSMs scale poorly with combinatoric modes. Statecharts (Harel) add nesting + history + orthogonal regions; libraries like Boost.SML implement this.
  • Self-transitions mid-method — state_ reassigned while the old state's method is still on the stack. Use std::move carefully; don't access *this-derived members of the old state after the swap.
  • Transition table duplication — state-decides spreads transitions across many files. Context-owned table centralizes but couples context to every state.
  • Initial state ambiguity — always construct context with an explicit initial state, not a default-null pointer.

7. Related Patterns

  • State vs Strategy — same diagram, different intent. Strategy: client picks the algorithm; algorithms don't swap themselves. State: state object itself triggers transitions based on internal flow. Strategy choice is configuration; state choice is lifecycle.
  • Singleton-like stateless states — overlaps with Flyweight when states have no per-instance data.
  • Memento — pairs with State for snapshot/restore of the current state.
  • CRTP alternative — when transitions are statically known, a CRTP-based FSM (CRTP) gives zero-overhead dispatch without std::variant boilerplate.
  • std::variant + std::visit — see § 4; modern C++ default for closed-set FSMs.
  • Statecharts (Harel) — generalization to hierarchical + concurrent states; Boost.SML implements this.

8. References