Course contentsShow
← Rust
Lesson 488 of 1,9105. Type System and TraitsFree lesson

Defining Zero-Sized Types for State Machine Encoding

Use uninhabited types to track state changes at the type level without memory overhead.

In this lesson, you will learn how to use Zero-Sized Types (ZSTs) to enforce valid state transitions at compile time, ensuring your state machine is impossible to misuse.

The Concept: Types as States

When building a state machine, a common bug is calling a method when the system isn't ready (e.g., trying to send() a packet before it is connected()). Instead of using runtime checks like if state == State::Connected, we can represent each state as a unique struct with no fields.

Because these structs are ZSTs, they occupy zero bytes of memory. The compiler uses them purely for "type-level bookkeeping."

Implementing Type-Level Transitions

Consider a simple connection state machine:

struct Disconnected;
struct Connected;

struct Socket<State> {
    _state: std::marker::PhantomData<State>,
}

impl Socket<Disconnected> {
    fn new() -> Self {
        Socket { _state: std::marker::PhantomData }
    }

    fn connect(self) -> Socket<Connected> {
        Socket { _state: std::marker::PhantomData }
    }
}

impl Socket<Connected> {
    fn send(&self, data: &str) {
        println!("Sending: {}", data);
    }
}

Why This Works

  1. Zero Memory Overhead: Because Disconnected and Connected have no data, the Socket struct remains the exact same size regardless of which state it is in.
  2. Compile-Time Safety: If you try to call socket.send() while it is in the Disconnected state, the Rust compiler will throw an error. The send method simply doesn't exist for Socket<Disconnected>.
  3. State Consumption: Notice that connect takes self (consuming the Disconnected instance) and returns a new Socket<Connected>. This is the "State Pattern" enforced by the borrow checker. You cannot accidentally use the old disconnected socket again because it has been moved into the transition.

By shifting the burden of state validation from the CPU to the compiler, we create faster, more reliable code that requires zero runtime branching.

Key Takeaway: Use empty structs and PhantomData to encode your state machine's transitions into the type system, transforming runtime errors into compile-time impossibilities without increasing your memory footprint.