When names matter
You're writing a function to parse a CSV line. It needs to return the parsed row and a flag indicating if the line was malformed. You grab a tuple for the quick return. (String, bool) gets the job done. Six months later, you add a confidence score to the parser. You change the return type to (String, bool, f64). You update the function. You forget to update the caller. The caller reads the boolean as the confidence score. The logic breaks silently.
Now imagine you used a struct. struct ParseResult { text: String, valid: bool, confidence: f64 }. You add the confidence field. The compiler screams E0063 (missing field) at every construction site. You fix the errors. The code compiles. The struct saved you from a runtime bug by forcing you to acknowledge the change.
This is the core tension. Tuples offer speed and brevity. Structs offer safety and clarity. The choice depends on how long the data lives and how much meaning the fields carry.
Named slots versus ordered slots
A struct is a named container with labeled fields. Every field has a name and a type. You access data by name. A tuple is an anonymous container with a fixed number of slots. Fields have no names. You access data by index, starting at zero.
Think of a struct as a form with labeled fields. You fill in "Name" and "Age". The labels make it clear what goes where, even if you shuffle the order of the fields on the form. A tuple is like a stack of index cards. The top card is the first value, the second card is the second value. The meaning comes from the order. If you swap the cards, the meaning changes.
Structs are for data with semantic meaning. Tuples are for temporary groupings where order is the only invariant.
Minimal examples
Here is a struct. It has a name and labeled fields.
/// A point in 2D space.
/// Fields are named for clarity and self-documentation.
struct Point {
x: i32,
y: i32,
}
fn main() {
// Initialize with named fields.
// The order of fields in the literal does not matter.
// The compiler matches names to slots.
let p = Point { y: 20, x: 10 };
// Access by name.
// The code reads like English.
println!("X coordinate: {}", p.x);
}
Here is a tuple. It has no name and no labels.
fn main() {
// Tuple with two integers.
// No names. Order defines meaning.
let t = (10, 20);
// Access by index.
// t.0 is the first element.
// t.1 is the second element.
println!("First element: {}", t.0);
}
Struct initialization allows reordering. Tuple initialization requires strict order. This flexibility makes structs safer for long-lived data. Tuples force discipline on order, which is useful for short-lived data where order is the only invariant.
Memory and performance
Here is the surprising part. If you define struct Point { x: i32, y: i32 } and let t: (i32, i32), the memory layout is identical. Both store two i32s back-to-back in memory. The CPU sees the same bytes. There is zero runtime cost to using a struct over a tuple.
The difference exists only in the compiler's type checker. Structs give you safety and readability for free. You never pay a performance penalty for choosing a struct. The overhead is purely compile-time type checking, which happens before your code runs.
This means you should never choose a tuple for performance reasons. If you think a tuple is faster, you are wrong. The compiler generates the same machine code for equivalent data layouts. Choose based on maintainability, not speed.
Realistic usage
In real code, structs model domain concepts. Tuples group temporary results.
/// Configuration for a network client.
/// Uses a struct because fields have meaning and may grow.
/// The name ClientConfig documents the purpose.
struct ClientConfig {
host: String,
port: u16,
timeout_ms: u64,
}
/// Parses a configuration line.
/// Returns a tuple because we are grouping a result and a status.
/// The tuple is temporary and will be destructured immediately.
fn parse_line(line: &str) -> (ClientConfig, bool) {
// ... parsing logic ...
// Return tuple for brevity.
// The caller will likely destructure this right away.
// Using a struct here would add ceremony for no gain.
(
ClientConfig {
host: "example.com".into(),
port: 80,
timeout_ms: 1000
},
true
)
}
fn main() {
// Destructure the tuple immediately.
// Bind values to named variables.
// This is the community convention for tuples.
let (config, is_valid) = parse_line("example.com:80");
// Now we have named variables.
// The tuple served its purpose and vanished.
if is_valid {
println!("Connecting to {}", config.host);
}
}
The ClientConfig is a struct because it represents a concept. It has fields with names. It might grow with new fields like retries or user_agent. The parse_line return value is a tuple because it groups a result and a flag for a single purpose. The caller destructures it immediately. The tuple doesn't need a name.
The hybrid: tuple structs
Sometimes you want a named type but don't care about field names. Enter the tuple struct.
A tuple struct is a struct with a name but indexed fields. It bridges the gap between structs and tuples.
/// A color represented as RGB components.
/// Uses a tuple struct because the fields are a sequence without distinct names.
/// The type name provides semantic meaning.
struct Color(u8, u8, u8);
fn main() {
// Construct like a tuple, but with a type name.
// The type is Color, not (u8, u8, u8).
let red = Color(255, 0, 0);
// Access by index.
println!("Red component: {}", red.0);
// You can implement methods on tuple structs.
// This is impossible with plain tuples.
let brightness = red.brightness();
println!("Brightness: {}", brightness);
}
impl Color {
/// Calculates the average brightness of the color.
fn brightness(&self) -> u8 {
(self.0 + self.1 + self.2) / 3
}
}
Tuple structs are useful when you need a distinct type for trait implementations or type safety, but the fields are just a sequence. You get the type name and method support of a struct with the brevity of a tuple.
Pitfalls and compiler errors
Tuples introduce magic numbers. t.2 means nothing without context. If you add a field to a tuple, you change the type entirely. (i32, i32) is not (i32, i32, i32). The compiler sees them as unrelated types. Every usage breaks. You get E0308 (mismatched types) everywhere. Refactoring a tuple is painful.
Structs are additive. Adding a field to a struct keeps the type name. You get E0063 (missing field) at construction sites. Usage sites like p.x still work. Refactoring a struct is guided by the compiler.
Accessing a non-existent field on a struct gives E0609 (no field). Accessing an out-of-range index on a tuple gives E0608 (index out of range). Both errors are caught at compile time. The struct error tells you the name is wrong. The tuple error tells you the index is wrong. Struct errors are more informative.
Convention aside: The community convention for tuples is to destructure them as soon as possible. Holding onto a tuple variable for long is frowned upon. Bind the values to named variables right away. If you find yourself passing a tuple around without destructuring, you probably need a struct.
Decision matrix
Use a struct when the data represents a domain concept with named attributes, like a User or a Point. Use a struct when you expect the data shape to evolve, adding or removing fields over time. Use a struct when you need to attach behavior via methods or implement traits. Use a struct when the data is passed across multiple function boundaries. Use a tuple when grouping a small number of heterogeneous values for a single, immediate purpose. Use a tuple when returning multiple values from a function, especially when destructuring at the call site. Use a tuple when the order of elements carries the meaning and naming them would be redundant. Use a tuple struct when you need a distinct type for type safety but the fields are a sequence without distinct names.
Names are cheap. Pay for them. If you find yourself writing t.2, you probably need a struct.