Unit structs are struct definitions with no fields, declared using struct Name;. They are primarily used to define types that carry no data but need to exist for type safety, such as implementing traits for specific types, creating singleton markers, or defining custom error types.
You typically encounter them when you need a distinct type to satisfy a trait bound without storing state, or when implementing the "newtype pattern" for zero-cost abstractions where the inner type is irrelevant. They are also the standard way to define custom error types in thiserror or anyhow when the error itself doesn't need to hold specific data.
Here is a practical example using a unit struct as a marker type to enable a specific behavior only for that type:
// Define a unit struct as a marker type
struct DebugOnly;
// Implement a trait specifically for this marker
trait Loggable {
fn log(&self);
}
impl Loggable for DebugOnly {
fn log(&self) {
println!("Debug mode active");
}
}
fn main() {
let marker = DebugOnly;
marker.log(); // Works because DebugOnly implements Loggable
// This would fail to compile if we tried to call log() on a generic type
// that doesn't implement Loggable, enforcing type safety.
}
Another common use case is defining custom error types where the error message is static or handled externally:
#[derive(Debug)]
struct ConnectionTimeout;
impl std::fmt::Display for ConnectionTimeout {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Connection timed out")
}
}
impl std::error::Error for ConnectionTimeout {}
fn main() -> Result<(), Box<dyn std::error::Error>> {
Err(Box::new(ConnectionTimeout))
}
Use unit structs whenever you need a unique type identity without the overhead of fields. They are essential for enforcing compile-time constraints, creating type-safe markers, and defining lightweight error variants. Avoid them if you need to store any data, as adding a single field changes the struct's memory layout and semantics entirely.