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>.
| Mistake | Error |
|---|---|
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 extend | destructor '~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
| Event | What is destroyed |
|---|---|
| The owning scope ends — a block, a function, each pass of a loop body | every value it still owns |
return, break, continue, fail, ? propagation | the values of every scope being left |
= or <- into a live place | the old value, after the new one is produced |
A destructuring let or match arm | each part bound to _ or left out of the pattern |
| A by-value parameter | the parameter, when the function returns |
| Not destroyed | Why |
|---|---|
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 assigned | it holds nothing |
| The old contents of storage written through a raw pointer | a pointer cannot tell whether the storage holds a value |
| Anything, when the program panics or exits | panics 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
extendblock a destructor lives in - Panics — failures that end the program without cleanup
- Learn: Destructor