Structs

Unit-Like Structs

A unit-like struct is a named struct declared with no fields at all (struct Marker;). It is called "unit-like" because it behaves like the built-in unit type (), except that it defines a brand-new, distinct type in the type system.

Syntax and Zero-Sized Representation

A unit-like struct is declared with the struct keyword, its UpperCamelCase name, and a semicolon (;)—with no curly braces {} or parentheses (). Just like its type name, a value of a unit-like struct is constructed by writing its bare name:

use std::mem::size_of;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Authenticated;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Guest;

fn main() {
    // `Authenticated` is BOTH the type name and the value constructor!
    let token: Authenticated = Authenticated;

    println!("Token: {token:?}");
    println!("size_of::<Authenticated>() = {} bytes", size_of::<Authenticated>());
}
Token: Authenticated
size_of::<Authenticated>() = 0 bytes

Because a unit-like struct has zero fields and only one possible value, it is a Zero-Sized Type (ZST) that occupies 0 bytes in memory at runtime! Creating, passing, or storing a unit-like struct in a variable or struct field generates zero machine instructions and consumes zero stack or heap bytes.

Primary Use Cases for Unit-Like Structs

1. Implementing a Trait on a Stateless Type (Strategy / Policy Types)

Often a trait defines behavior (such as a formatting policy, hashing algorithm, or event handler) that requires no per-instance state fields. A unit-like struct gives you a concrete type on which to implement the trait at zero memory cost:

trait Formatter {
    fn format_kv(&self, key: &str, value: &str) -> String;
}

struct JsonFormatter;
struct EnvFormatter;

impl Formatter for JsonFormatter {
    fn format_kv(&self, key: &str, value: &str) -> String {
        format!("{{\"{key}\": \"{value}\"}}")
    }
}

impl Formatter for EnvFormatter {
    fn format_kv(&self, key: &str, value: &str) -> String {
        format!("{}={value}", key.to_ascii_uppercase())
    }
}

fn print_setting<F: Formatter>(formatter: F, key: &str, value: &str) {
    println!("{}", formatter.format_kv(key, value));
}

fn main() {
    print_setting(JsonFormatter, "host", "localhost");
    print_setting(EnvFormatter, "host", "localhost");
}
{"host": "localhost"}
HOST=localhost

2. Custom Zero-Sized Error Types

When a fallible function can fail for only a single obvious reason (such as parsing a specific marker or validating a non-zero integer), a unit-like struct serves as a descriptive, 0-byte error type for Result<T, E>:

use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DivisionByZeroError;

impl fmt::Display for DivisionByZeroError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "cannot divide by zero")
    }
}

fn divide(a: i32, b: i32) -> Result<i32, DivisionByZeroError> {
    if b == 0 {
        Err(DivisionByZeroError)
    } else {
        Ok(a / b)
    }
}

fn main() {
    println!("10 / 2 = {:?}", divide(10, 2));
    println!("10 / 0 = {:?}", divide(10, 0));
}
10 / 2 = Ok(5)
10 / 0 = Err(DivisionByZeroError)

3. Compile-Time State Machines (The Typestate Pattern)

Unit-like structs are used as generic type parameters (struct Connection<State>) to encode state transitions at compile time so invalid method calls (such as sending data on a disconnected socket) are rejected by rustc with zero runtime memory overhead:

use std::marker::PhantomData;

struct Disconnected;
struct Connected;

struct Channel<State> {
    address: String,
    _state: PhantomData<State>, // 0 bytes!
}

impl Channel<Disconnected> {
    fn new(address: &str) -> Self {
        Self { address: address.to_string(), _state: PhantomData }
    }

    fn connect(self) -> Channel<Connected> {
        println!("Connected to {}", self.address);
        Channel { address: self.address, _state: PhantomData }
    }
}

impl Channel<Connected> {
    // `.send()` ONLY exists on `Channel<Connected>`!
    fn send(&self, msg: &str) {
        println!("Sent to {}: {msg}", self.address);
    }
}

fn main() {
    let ch: Channel<Disconnected> = Channel::new("127.0.0.1:9000");
    // `ch.send("hi");` would not compile here because `ch` is `Channel<Disconnected>`!
    let active: Channel<Connected> = ch.connect();
    active.send("Hello, typestate!");
}
Connected to 127.0.0.1:9000
Sent to 127.0.0.1:9000: Hello, typestate!