Destructors

A destructor is code the compiler runs when a value's life ends. It is declared in an extend block, named after its type with a leading ~, and it borrows the dying value mutably. Nothing calls it by name: the compiler invokes it exactly once for each initialized value that still owns its state, and never for a value that was moved away or never initialized.

Syntax

extend T {
    func ~T(self: &var T) { … }
}
struct Guest {
    name: char8[..];
}

extend Guest {
    func ~Guest(self: &var Guest) {
        PrintLine("{} leaves", self.name);
    }
}

The shape is fixed: one receiver, self: &var T, no other parameters, no result, no type parameters, and a body. For a generic type the name carries no type arguments: func ~Store(self: &var Store<T>) inside extend Store<T>.

MistakeError
func ~Guest(self: &Guest)destructor for type 'Guest' must have signature 'func ~Guest(self: &var Guest)'
func ~Visitor(self: &var Host)destructor '~Visitor' must be named '~Host' for type 'Host'
func ~Ghost(self: &var Ghost);destructor '~Ghost' must have a body
a destructor outside extenddestructor '~Item' may only be declared in an extend block
seat.~Guest();expected a field name or tuple index after '.' before '~'

A destructor cannot be called. To end a value's life early, replace it or move it into a function that lets it go.

When destruction runs

EventWhat is destroyed
The owning scope ends — a block, a function, each pass of a loop bodyevery value it still owns
return, break, continue, fail, ? propagationthe values of every scope being left
= or <- into a live placethe old value, after the new one is produced
A destructuring let or match armeach part bound to _ or left out of the pattern
A by-value parameterthe parameter, when the function returns
Not destroyedWhy
A value moved away with <-its new owner destroys it
A value returned with return <-value;the caller owns it now
Storage declared without a value and never assignedit holds nothing
The old contents of storage written through a raw pointera pointer cannot tell whether the storage holds a value
Anything, when the program panics or exitspanics do not unwind

A write through a raw pointer — *p <- value, p[i] = value, or a field reached through *var T — initializes the storage it addresses and destroys nothing. Code that replaces a value through a pointer destroys or moves out the old one first.

Order

Values that end together are destroyed in reverse order of creation, newest first: a value made later may depend on one made earlier, so it has to go while the earlier one is still there.

A value's own destructor runs first, while its fields are still intact. Then the compiler destroys its contents — fields, tuple and array elements, and the payload of a variant case or optional — in reverse construction order: the last field first, the last element first.

struct Part {
    name: char8[..];
}

extend Part {
    func ~Part(self: &var Part) {
        PrintLine("~Part {}", self.name);
    }
}

struct Machine {
    first: Part;
    second: Part;
}

extend Machine {
    func ~Machine(self: &var Machine) {
        PrintLine("~Machine");
    }
}

func Build() {
    let machine = Machine { first: Part { name: "first" }, second: Part { name: "second" } };
    let a = Part { name: "a" };
    let b = Part { name: "b" };
}

Build prints ~Part b, ~Part a, ~Machine, ~Part second, ~Part first.

A type with no destructor of its own still has its droppable parts destroyed: the compiler generates that cleanup for every structure, tuple, array, variant and optional that contains something to destroy.

Replacement

Assigning to a place that holds a value replaces it in three steps: the new value is produced — a custom copy runs here — then the old value is destroyed, then the new one is installed. A field, a tuple element and an element of an array or slice are places like any other, whether they belong to a local or are reached through a &var reference:

var seat = Guest { name: "Bob" };
seat = Guest { name: "Cy" };    // Bob leaves here

A part of a local holds a value only while the local holds one. A droppable local declared without a value, or moved from, is assigned whole before any part of it is written, because a part written into empty storage would never be destroyed:

error: cannot write field 'guest' of 'table', which holds no value
  note: 'table' was declared without a value at …
  note: 'Table' needs destruction, and a value written into a part of storage that holds none would never be destroyed
  help: initialize 'table' whole, as in 'table = Table { ... }'

A type with nothing to destroy may still be filled one part at a time; see Initialization.

Drop flags

Whether a local still owns its value can depend on the path taken. When a value is moved on only some paths, the compiler keeps a drop flag for it, and the flag — not the source text — decides at the end of the scope whether the value is destroyed:

func Visit(away: bool) {
    let guest = Guest { name: "Dee" };
    if away {
        let taken <- guest;    // destroyed here, as 'taken'
        return;
    }
}                              // destroyed here only when not moved

Either way Dee leaves is printed once. A local's flag covers the whole value, which is why a part cannot be moved out on its own.

Copies and destructors

A copy is a second value with its own end, so a copyable type with a destructor runs it once per copy. For a type that releases a resource — frees memory, closes a file — that means releasing it twice. Such a type prohibits copying with a bodyless func =(self: &var T, other: &T);, or implements a copy that duplicates the resource; see Copy and move.

No unwinding

A panic stops the program where it happens. Nothing is unwound: no destructor runs and no deferred statement runs, in the panicking function or in any caller. The same holds for ending the process. Cleanup that must survive a failure belongs on an error path — fail and ? do run destructors.

See also

  • Defer — cleanup tied to a scope, and how it is ordered against destructors
  • Copy and move — prohibiting copies of a type that owns a resource
  • Extensions — the extend block a destructor lives in
  • Panics — failures that end the program without cleanup
  • Learn: Destructor