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!