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
- 2. Structure
- 3. C++ Example
- 4. Modern Variant with
std::variant - 5. When to Use / When Not To
- 6. Variants and Pitfalls
- 7. Related Patterns
- 8. References
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 (vimNormal/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 atransitionTo(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/ifchains 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
ifis 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. Usestd::movecarefully; 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::variantboilerplate. 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
- Gamma, Helm, Johnson, Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. State chapter.
- cppreference: std::variant
- cppreference: std::visit
- Boost.SML — State Machine Language
- Harel, Statecharts: A Visual Formalism for Complex Systems, 1987.