Skip to content
CryoCryo home
LanguageControl and data

8Structs

A struct is a value type with named fields and (optionally) methods. Structs live on the stack, are passed by value, and are the right choice for plain data.

8.1 Declaration

type struct Point {
    x: int;
    y: int;
}

8.2 Fields and Visibility

Struct fields are public by default - readable and writable wherever the struct itself is visible. Restrict a field with private; a private field is then accessible only from the module that declares the struct (enforced as E0353). Calling a function-typed field, h.run(1), reads it, so a private one is refused outside that module as reading it is. Which module code is in is the module whose file it is written in, not the type it belongs to: a free function beside the struct may read the field, and an implement block written in another module may not, however it names the type. Visibility blocks group fields that share an access level:

type struct Rect {
private:
    cached_area: int;   // only code in Rect's own module may touch this
public:
    width:  int;
    height: int;
}

A struct literal sets every field of the struct, including the ones it leaves to their defaults, so a literal may be written only where every field is visible. One private field and code outside the module cannot write the literal at all; it goes through a constructor the module provides. Setting a private field is the more dangerous operation, not the less: reading one leaks information, while setting one breaks the invariant the field exists to protect, so there is no exception for construction.

// module app::money
type struct Cents {
private:
    n: i64;
public:
    static of(n: i64) -> Cents { return Cents { n: n }; }   // the module builds it
}

// module app::report
const c: Cents = Cents { n: 5 };       // E0353: field `n` of `Cents` is private
const d: Cents = Cents::of(5);         // fine

Visibility may also be declared per-field with a leading private / public. Within a struct, only public: and private: blocks are valid; protected: is refused (E0117).

A method is declared in the module its declaration is written in: a private method in an implement block is private to the block's module, not to the module of the type it extends.

Field visibility (a private field, E0353) is a different axis from a top-level type being private (E0503 - see section 13.4); both are scoped to the declaring module. A public struct may have private fields, and a private struct's fields are public to the rest of its own module.

Fields may declare default values with = <expr>. When a struct literal omits a field that has a default, the default is used; a field with no default must still be supplied, and omitting one is E0355. A default is a standalone expression - it cannot reference the other fields - and is evaluated afresh at each construction that omits the field.

type struct Config {
    debug:   boolean = false;
    verbose: boolean = false;
    retries: i32     = 3;
}

const c: Config = Config {};              // -> { false, false, 3 }
const d: Config = Config { retries: 5 };  // -> { false, false, 5 } (supplied value wins)
// Omitting a field that has NO default is still `E0355`.

8.3 Methods

Methods are functions inside a struct body whose first parameter declares how the method takes the receiver:

  • &this: shared (read-only) borrow. The body may not modify fields.
  • mut &this: exclusive (mutating) borrow. The body may modify fields.
  • this / mut this: by-value (consuming) receiver. The receiver is moved into the method; the caller's value is consumed and may not be used afterward. Use this for a method that dismantles a value - unwrapping it into its parts, or handing its storage off. mut this additionally lets the body reassign the receiver binding.
type struct Rect {
    width:  int;
    height: int;

    area(&this) -> int {
        return this.width * this.height;
    }

    scale(mut &this, factor: int) -> void {
        this.width  = this.width  * factor;
        this.height = this.height * factor;
    }
}

The receiver shape is part of the signature, so a caller knows whether a method borrows or consumes the receiver without reading the body.

A method is reached only by calling it. r.area with no call, where area is a method and no field has that name, names no value and is refused (E0204), with a suggestion to write r.area().

Struct-destructuring bindings

A const/mut binding may use a destructuring pattern - { field, field, ... } with a type annotation naming the struct - to move a struct's fields out into individually named locals. Each field is moved into a like-named local (field order need not match the declaration). This is the idiomatic companion to a consuming (this) receiver: binding the fields marks the receiver as fully consumed, so its automatic drop is suppressed and the fields can be handed off without a double free. Any bound field you don't move onward is dropped normally at the end of scope.

type struct Box<T, A = GlobalAlloc> {
    ptr:   T*;
    alloc: A;

    // Consume the Box and return its raw pointer, transferring ownership
    // to the caller. Destructuring `this` moves both fields out, so the
    // Box itself is not dropped (that would free `ptr`); `alloc` is dropped
    // at function end (a no-op for GlobalAlloc).
    into_raw(mut this) -> T* {
        const { ptr, alloc }: Box<T, A> = this;
        return ptr;
    }
}

8.4 Static Methods

A static method belongs to the type itself, not an instance. It is called with ::.

type struct Point {
    x: int;
    y: int;

    static new(x: int, y: int) -> Point {
        return Point { x: x, y: y };
    }

    static origin() -> Point {
        return Point { x: 0, y: 0 };
    }
}

const p: Point = Point::new(10, 20);

A struct has no constructor; a static new(...) factory is the idiom, and simply returns a struct literal.

8.5 Struct Literals

A struct literal names each field inside braces. Field order does not need to match declaration order, but every non-defaulted field must be specified.

const p: Point = Point { x: 10, y: 20 };

8.6 Generic Structs

type struct Pair<T> {
    first:  T;
    second: T;

    static new(a: T, b: T) -> Pair<T> {
        return Pair { first: a, second: b };
    }

    swap(mut &this) -> void {
        const temp: T = this.first;
        this.first  = this.second;
        this.second = temp;
    }
}

const ints: Pair<int>    = Pair::<int>::new(1, 2);
const strs: Pair<string> = Pair::<string>::new("hello", "world");

Pair<int> and Pair<string> are independent types. See section 11.6.

8.7 Unions

A type union is an untagged, C-style union: all fields occupy the same storage, overlapping at offset 0. The union's size is that of its largest member and its alignment that of its most-aligned member.

type union Value {
    i: i64;
    f: f64;
    bytes: u8;
}

Here sizeof(Value) == 8 - the size of the largest member (i64/f64), not their sum. Writing one member and reading another reinterprets the shared bytes:

mut v: Value = Value { i: 0 };
v.f = 1.5;          // writes the 8-byte storage as an f64
const bits: i64 = v.i;   // reads the same bytes back as an i64

A union is untagged: it carries no discriminant recording which member is active, so reading a member other than the one last written is the programmer's responsibility (the reinterpretation is well-defined; whether it's meaningful is not checked). When you want a tagged, exhaustively-checked sum type, use type enum instead.

Literals. A union literal initialises exactly one field; naming zero or more than one is a compile error (E0363):

const ok:  Value = Value { i: 42 };       // OK
// const bad: Value = Value { i: 1, f: 2.0 };  // error E0363: exactly one field

Methods. Like structs, unions may declare methods inline (instance and static), or in an implement block:

type union Tagged {
    raw: i64;
    handle: i64;

    static from_raw(n: i64) -> Tagged { return Tagged { raw: n }; }
    get(&this) -> i64 { return this.raw; }
}

Generics. Unions may be parameterised:

type union Either<A, B> {
    a: A;
    b: B;
}

const e: Either<i64, f64> = Either::<i64, f64> { a: 100 };

Layout control. ![repr(c)] and ![align(N)] apply to unions exactly as they do to structs (see section 16).

Matching. A union value is not matched variant-wise the way an enum is (there is no discriminant). You match on a member's value - e.g. match (v.i) { 0 => ..., _ => ... } - and static match (T) works inside a generic union's methods.

Ownership. A union's members are never auto-dropped, since the active member is unknown. If a union owns a resource, implement Drop for it (implement trait Drop for Handle { drop(mut &this) -> void { ... } }); an inherent method merely named drop is not a destructor (section 15.2).