State

Learning Objectives

Let an object change behaviour when its internal state changes.

Compare state objects with enum + switch.

Avoid over-classifying simple states.

Prerequisites: Modules 24, 40.

Intuition and Definition

Intent: allow an object to alter its behaviour when its internal state changes; the object appears to change its class.

Participants: Context (holds the current state), State interface (one method per event), ConcreteStates (implement behaviour for that state and decide transitions).

Why It Matters

Lifecycle logic (ignition, connections, orders, vending machines) written as nested if (state == …) in every method becomes unmaintainable. With State, each state's behaviour lives in one place.

Syntax and Language Notes

# a table-driven alternative in Python
transitions = {("off", "press"): "acc", ("acc", "press"): "on", ("on", "press"): "off"}

Complete Example

#include <iostream>
#include <memory>
#include <string>

class Ignition;

struct IgnitionState {
    virtual ~IgnitionState() = default;
    virtual std::string name() const = 0;
    virtual void pressStart(Ignition& ctx, bool brakePressed) = 0;
};

class Ignition {                                                  // Context
    std::unique_ptr<IgnitionState> state_;
public:
    Ignition();
    void set(std::unique_ptr<IgnitionState> s) { std::cout << "  " << state_->name() << " -> " << s->name() << "\n"; state_ = std::move(s); }
    void pressStart(bool brake) { state_->pressStart(*this, brake); }
    std::string name() const { return state_->name(); }
};

struct Off : IgnitionState { std::string name() const override { return "OFF"; } void pressStart(Ignition& c, bool brake) override; };
struct Accessory : IgnitionState { std::string name() const override { return "ACC"; } void pressStart(Ignition& c, bool brake) override; };
struct EngineOn : IgnitionState { std::string name() const override { return "ENGINE ON"; } void pressStart(Ignition& c, bool brake) override; };

void Off::pressStart(Ignition& c, bool brake) {
    if (brake) c.set(std::make_unique<EngineOn>());            
…
press (no brake):
  OFF -> ACC
press (no brake):
  ACC -> OFF
press (brake):
  OFF -> ENGINE ON
press:
  ENGINE ON -> OFF
final state: OFF

Step-by-Step Walkthrough

The context delegates pressStart to the current state object.

Each state decides the next state; the rule "brake + start cranks the engine" lives only in Off and Accessory.

Adding a CrankingFailed state means one new class and changes only in the states that can reach it.

flowchart LR
  OFF -->|press| ACC
  OFF -->|press + brake| ON[ENGINE ON]
  ACC -->|press| OFF
  ACC -->|press + brake| ON
  ON -->|press| OFF

Common Mistakes

State classes for two trivial states — a bool and an if are clearer.

Transition logic duplicated in the context and the states.

Allocating a new state object per transition in hot paths — use stateless singletons per state or a table.

Hidden states (combinations of flags) not modelled explicitly.

Trade-offs and Alternatives

Few states, little behaviour per state Transition table (Module 24)

Many states, uniform transitions, data-driven configuration Rich behaviour per state that differs a lot std::variant of state structs

Closed set, value semantics, state-specific data

Real-World Use

TCP connection states, ignition and power modes in vehicles, vending machines, document workflows, game character behaviour, UI wizards.

Practice

Q1. (Easy) Who decides the next state in this implementation?

Answer: The current state object.

Q2. (Medium) State vs Strategy?

Answer: Strategies are chosen by clients and usually don't know each other; states are switched by the context/states themselves as events occur and typically know their successors.

Q3. (Advanced) Implement the same ignition logic with std::variant and std::visit.

#include <iostream>
#include <variant>
struct Off {}; struct Acc {}; struct On {};
using Ign = std::variant<Off, Acc, On>;
template <class... F> struct overloaded : F... { using F::operator()...; };
Ign press(const Ign& s, bool brake) {
    return std::visit(overloaded{
        [&](Off) -> Ign { return brake ? Ign{On{}} : Ign{Acc{}}; },
        [&](Acc) -> Ign { return brake ? Ign{On{}} : Ign{Off{}}; },
        [&](On)  -> Ign { return Off{}; }}, s);
}
const char* name(const Ign& s) { return std::visit(overloaded{[](Off) { return "OFF"; }, [](Acc) { return "ACC"; }, [](On) { return "ENGINE ON"; }}, s); }
int main() {
    Ign s = Off{};
    for (bool brake : {false, false, true, false}) { s = press(s, brake); std::cout << name(s) << " "; }
    std::cout << "\n";
}
ACC OFF ENGINE ON OFF

Interview Question

Q1. "Design a vending machine. Would you use the State pattern?"

Answer: Identify states (Idle, HasMoney, Dispensing, OutOfStock, Maintenance) and events (insertCoin, select, cancel, refill). If behaviour per state differs substantially and the state count may grow, use State objects so each state's handling of every event is in one class; otherwise a transition table keyed by (state, event) is simpler and data-driven. Either way, keep transitions in one place, make illegal events explicit (reject with a reason), and test every state × event pair.

Quick Quiz

Q1. What does the context delegate to?

Answer: Its current state object.

Q2. Name two alternatives to the State pattern.

Answer: enum + switch, transition tables (or std::variant).

Q3. When is State over-engineering?

Answer: With very few states and little state-specific behaviour.

Summary

State moves state-specific behaviour into objects and makes transitions explicit.

Use tables or variants for simpler cases.

Related: Modules 24, 40, 85 (simulators).