Structs and field initialisation

In Rust a struct groups named values into one type. It has no inheritance, no constructors in the C++ sense and no hidden initialisation: a value is built by a literal that names every field, and the compiler checks that none is missing. That is less machinery than a class and more than a tuple, because the fields have names.

Coming from C++? Structs and classes puts a class with private members and a Rust struct with an impl block side by side, and then leaves a field out in both languages: one prints zero, the other refuses to build.

This lesson covers the value itself: declaration, literal, shorthand, update syntax, tuple structs and the field that is missing. Methods come next, because they need their own lesson.

Named fields

A struct declaration lists the fields and their types. The literal repeats the field names with values, in any order, and the result is a value of that type. Reading a field is a dot and a name, which is the only access there is.

The program below builds a point with two integers and prints both fields. Nothing is allocated: the struct is two integers on the stack, laid out in declaration order.

struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 3, y: 4 }; println!("{} {}", p.x, p.y); }

rustc --edition 2024 named.rs
./named

3 4

A struct can derive traits with an attribute, which the lesson on derive covers. Without an attribute the type has no Debug and no Clone, which is the default the language chose on purpose.

Shorthand and update syntax

When a variable has the same name as the field, the literal can write the name once: Point { x, y }. The update syntax ..a fills the remaining fields from another value, which is the closest thing Rust has to a copy of a struct with one change.

The two forms are in the program below. The update takes the fields by value, so it moves them; for a struct of Copy fields the old value stays usable, which is why both points print afterwards.

struct Point { x: i32, y: i32, } fn main() { let x = 3; let y = 4; let a = Point { x, y }; let b = Point { x: 10, ..a }; println!("{} {} {} {}", a.x, a.y, b.x, b.y); }

rustc --edition 2024 shorthand.rs
./shorthand

3 4 10 4

With a String field the update would move that field out of the old value, and the old value could not be used as a whole again. The lesson on ownership explains why that is a move and not a copy.

Tuple structs

A tuple struct has fields without names, and the fields are reached by index. It is the shape to use for a newtype, a wrapper that gives a plain value a distinct type: Meters(f64) cannot be confused with a bare f64 or with another unit.

The program builds one and prints its single field with .0. The wrapper costs nothing at run time: it is the same f64 with a different type.

struct Meters(f64); fn main() { let distance = Meters(3.5); println!("{}", distance.0); }

rustc --edition 2024 tuple_struct.rs
./tuple_struct

3.5

A tuple struct can have more than one field, and it is often used to return several values with names, which the lesson on tuples compares with a plain tuple.

Changing a field

A field is writable only if the binding is mutable. let mut counter allows counter.value += 1, and the field changes in place; with a plain let the same line is E0594, which the lesson on const and mutability reads in full.

The program increments twice and prints two. The struct itself was not replaced: only the field changed, which is why the binding has to be mutable.

struct Counter { value: u32, } fn main() { let mut counter = Counter { value: 0 }; counter.value += 1; counter.value += 1; println!("{}", counter.value); }

rustc --edition 2024 mutable_fields.rs
./mutable_fields

2

A method that changes a field takes &mut self, and the next lesson builds one. The rule is the same: the receiver must be a mutable binding at the call site.

A field left out

A struct literal must name every field. Leaving one out is E0063, and the message points at the literal and names the missing field. There is no default value unless the type implements Default and the literal asks for it.

The program below builds a point with only x. The compiler refuses, which is the opposite of the C++ aggregate initialisation that fills the rest with zero.

struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 3 }; println!("{}", p.x); }

rustc --edition 2024 missing_field_err.rs

error[E0063]: missing field `y` in initializer of `Point`
--> missing_field_err.rs:7:13
|
7 | let p = Point { x: 3 };
| ^^^^^ missing `y`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0063`.

The fix is to write the field, or to take the value from another instance with ..base, or to implement Default and use ..Default::default(). All three say what the missing value is instead of inventing one.

Exercises

  1. Add a third field to the first struct and fix every literal the compiler reports. How many errors did it report at once?
  2. Build a second point with the update syntax and change both fields. Does the first point change?
  3. Make the tuple struct a newtype for seconds and write a program that adds two of them. What does the compiler stop you from adding by mistake?

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. How is a struct value created in Rust?

    Answer

    By a literal that names its fields

    The struct literal names every field, and the compiler checks that none is missing.

  2. What does Point { x, y } mean when variables x and y exist?

    Answer

    It copies the variables into fields of the same name

    The shorthand writes the field name once and takes the value from the variable of that name.

  3. What does the update syntax ..base do?

    Answer

    It fills the remaining fields from base

    The fields not written in the literal are taken from base, by value.

  4. What is a tuple struct useful for?

    Answer

    Storing a value with a distinct type, such as Meters(f64)

    A newtype gives a plain value its own type, so it cannot be mixed up with another number.

  5. What does this program print? struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 3, y: 4 }; println!("{} {}", p.x, p.y); }

    Answer

    3 4

    The two fields print in the order the program names them.

  6. What does this program print? struct Point { x: i32, y: i32, } fn main() { let x = 3; let y = 4; let a = Point { x, y }; let b = Point { x: 10, ..a }; println!("{} {} {} {}", a.x, a.y, b.x, b.y); }

    Answer

    3 4 10 4

    The update copies the Copy fields, and only x differs in the second point.

  7. What does this program print? struct Meters(f64); fn main() { let distance = Meters(3.5); println!("{}", distance.0); }

    Answer

    3.5

    The single field is reached by index zero and prints its number.

  8. What does this program print? struct Counter { value: u32, } fn main() { let mut counter = Counter { value: 0 }; counter.value += 1; counter.value += 1; println!("{}", counter.value); }

    Answer

    2

    The mutable binding allows two increments of the field, so it ends at two.

  9. What is the first line of the compiler output for this program? struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 3 }; println!("{}", p.x); }

    Answer

    error[E0063]: missing field y in initializer of Point

    The literal does not name every field, and the error names the one that is missing.

  10. What does this program print? struct Pair { left: i32, right: i32, } fn main() { let base = Pair { left: 1, right: 2 }; let other = Pair { right: 9, ..base }; println!("{} {}", base.left, other.left + other.right); }

    Answer

    1 10

    The update keeps left from base, sets right to nine, and the second print adds one and nine to get ten.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev