Destructor
A destructor is code that runs when a value's life ends. Nobody calls it. The compiler does, exactly once for every value that is still alive when its life ends — when its scope closes, when a function returns, or when = replaces it with a new value.
In real code a destructor gives back what the value was holding: a file, a network connection, a block of memory. Because the compiler calls it, it cannot be forgotten, and it cannot be called twice. Here the destructor only prints, so that you can watch exactly when it runs.
Writing a destructor
A destructor is written inside extend, named after the type with a leading ~, and borrows the dying value mutably:
struct Guest {
name: char8[..];
}
extend Guest {
func ~Guest(self: &var Guest) {
PrintLine(" {} leaves", self.name);
}
}
It takes no other parameters and returns nothing. The signature is fixed: func ~T(self: &var T), with the type's own name.
When a scope ends
The guest in Visit lives only as long as the call:
func Visit(name: char8[..]) {
let guest = Guest { name: name };
PrintLine(" {} visits", guest.name);
}
a short visit:
Ada visits
Ada leaves
back in Main
"Ada leaves" is printed by the destructor as Visit returns, before Main carries on. The same happens at the end of every block that owns a value — including each pass of a loop body.
When a value is replaced
Replacing a value ends the old one's life first:
var seat = Guest { name: "Bob" };
seat = Guest { name: "Cy" };
Bob leaves at the =. Cy now holds the seat and will leave later. Replacing one field of a struct works the same way: the old field value is destroyed, the rest of the struct is untouched.
When a value is moved
After a move, only the new owner is destroyed:
let host = Guest { name: "Dee" };
let moved <- host;
host gave its value away, so there is nothing left in host to destroy. Dee leaves once, at the end of Main, as moved.
Newest first
Values that end together are destroyed in reverse order of creation — newest first. At the end of Main, seat (holding Cy) was created before moved (holding Dee), so Dee leaves first:
flowchart LR
subgraph made["Created, in order"]
direction LR
c1["seat — Cy"] --> c2["moved — Dee"]
end
subgraph gone["Destroyed at the end of Main"]
direction LR
d1["Dee leaves"] --> d2["Cy leaves"]
end
made -- "reversed" --> goneThe order is not arbitrary. A value made later may depend on one made earlier — a reader that uses an open file, say — so it has to go first, while the thing it depends on is still there.
| What happens | Destructor runs |
|---|---|
| The owning scope ends | yes, newest value first |
= replaces the value | yes, on the old value, at the assignment |
The value is moved with <- | not here — the new owner destroys it later |
| The value is copied | the copy is a second value, destroyed on its own |
Why this program never copies a guest
A copy of a guest would be a second guest, and it would leave too. With let twin = host;, the program would print "Dee leaves" twice for one person. For a guest that only prints, that is odd; for a value that frees memory or closes a file, it would free or close the same thing twice. The next lesson shows how a type turns copying off.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A destructor is code that runs when a value's life ends. It is written inside `extend`, named
// after the type with a leading `~`, and borrows the dying value mutably:
//
// func ~Guest(self: &var Guest) { ... }
//
// Nobody calls it. The compiler does, exactly once for every value that is still alive when its
// life ends: when its scope closes, when a function returns, or when `=` replaces it with a new
// value, whether it is a whole variable or one field of a struct. A value that was moved away is
// not destroyed where it was, because its new owner will destroy it later. Values that end
// together are destroyed in reverse order of creation, newest first.
//
// Here the destructor only prints, so its timing can be watched. In real code it gives back what
// the value was holding: a file, a connection, a block of memory. Notice that this program never
// copies a guest: a copy would be a second guest that leaves too. The NoCopy lesson deals with it.
import Io::PrintLine;
struct Guest {
name: char8[..];
}
extend Guest {
func ~Guest(self: &var Guest) {
PrintLine(" {} leaves", self.name);
}
}
// The guest lives only as long as this call.
func Visit(name: char8[..]) {
let guest = Guest { name: name };
PrintLine(" {} visits", guest.name);
}
func Main() -> int {
PrintLine("a short visit:");
Visit("Ada");
PrintLine(" back in Main");
// Replacing a value ends the old one's life first.
PrintLine("swapping the seat:");
var seat = Guest { name: "Bob" };
seat = Guest { name: "Cy" };
// After a move only the new owner is destroyed, so Dee leaves once, at the end.
PrintLine("moving:");
let host = Guest { name: "Dee" };
let moved <- host;
// `moved` was created after `seat`, so it is destroyed before it.
PrintLine("end of Main:");
return 0;
}
Run it
cd Examples/Ownership/Destructor
rux run
a short visit:
Ada visits
Ada leaves
back in Main
swapping the seat:
Bob leaves
moving:
end of Main:
Dee leaves
Cy leaves
Common mistakes
func ~Guest(self: &Guest) fails with error: destructor for type 'Guest' must have signature 'func ~Guest(self: &var Guest)'. The value is dying, and the destructor may need to change it on the way out — closing a handle, clearing a pointer — so it borrows it mutably.func ~Visitor(self: &var Guest) inside extend Guest fails with error: destructor '~Visitor' must be named '~Guest' for type 'Guest'.seat.~Guest(); does not even parse: error: expected a field name or tuple index after '.' before '~'. Destruction is the compiler's job. To end a value early, replace it with = or move it into a function that lets it go.let twin = host; compiles, and both twin and host are destroyed — the destructor runs twice for what you meant as one guest. For a type that holds a resource, prohibit copying, as in Non-copyable types.Try it yourself
- Create a guest inside a
forloop over0..3and print the pass number. When does each guest leave? - Add
let twin = host;before the move and count how many times Dee leaves. - Declare
struct Table { number: int32; guest: Guest; }, make avartable, and assign a new guest to itsguestfield. Who leaves, and when?
Learn more
- Move — why a moved-from value is not destroyed
- Non-copyable types — one value, one destructor call
- Defer — cleanup that belongs to a piece of work rather than a value
- Methods in the Rux Reference