When a value dies: the order of finalizers
On this page
For most values, the moment they die does not matter. For a value with a finalizer, a socket, a file, a lock, it is the moment the socket closes, the file is flushed or the lock is let go, so it is worth knowing exactly. This page gives that moment for every kind of value: a local, a temporary made in the middle of an expression, a value assigned over, a value in a region, an enum and a global. Afterwards you can read a block of code and say when, and in what order, each of its finalizers runs:
mod lifecycle;
import stdio use *;
struct Log {
pub n i32;
fn final(self &uni) { print <- " final ", n, "\n"; }
}
fn make(n i32) Log { print <- " make ", n, "\n"; new Log(n); }
fn take(l Log) { print <- " take ", l.n, "\n"; }
fn main() {
print <- "a field of a temporary:\n";
let n = make(1).n; // the Log dies at the end of this statement
print <- "moved into a call:\n";
take(make(2)); // take's parameter finalizes it
print <- "a condition:\n";
if make(3).n > 0 { // dies once the condition is decided, before the branch
print <- " branch\n";
}
print <- "assigned over:\n";
var v = make(4);
v = make(5); // 5 is made, then 4 finalized, then 5 stored
print <- "end of main:\n";
} // v, holding 5, dies here
It prints:
a field of a temporary: make 1 final 1 moved into a call: make 2 take 2 final 2 a condition: make 3 final 3 branch assigned over: make 4 make 5 final 4 end of main: final 5
Try this: move print <- "end of main:\n"; to just before v = make(5);, and predict where final 4 lands.
Locals and parameters#
Moves decide who holds a value; its holder decides when it dies. A local dies at the end of its block, the newest first, and a parameter at the end of its function, as take's did. A value that moved out of a variable belongs to its new holder, and the variable has nothing left to finalize; one that moved on only some paths is finalized where it is still held, which its drop flag decides. Whoever does it, the value's own finalizer runs first, then its fields' finalizers in the order the fields are declared, and so on down (Ending a value).
Temporaries#
A temporary is a value an expression produces that nothing takes: it is not bound to a variable, stored in a field or element, passed by value, returned or moved. A call's result whose field is read, an owning reference read through, an owner lent to a call as a borrowed reference, a value thrown away by a statement: each is a temporary, and dies at the end of the statement that produced it, the newest first. One produced by the condition of an if or a while dies once the condition is decided, before the branch runs, so each pass of a loop finalizes its own; and one produced by the right operand of and or or, which runs on some paths only, dies at the end of that operand. A value moved out of a temporary owner belongs to its new holder, and the owner's memory is freed without it. A temporary that a variable's initializer borrows lives on to the end of the variable's block instead (Borrowing a temporary).
A temporary that a raw pointer made from it may outlive is not finalized at all. Nothing follows a raw pointer (Pointers, in Safety), so the compiler leaves the value as it is rather than finalize it and leave the pointer dangling. An ordinary borrow of the same temporary does not keep it:
fn mk(n i32) R {..}
fn id(r &R) &R {..} // answers the borrow it is handed
let p = id(&mk(2)) as *R; // p may outlive the temporary: R's final never runs for it
let q = id(&mk(3)); // an ordinary borrow: R's final runs at the end of this statement
A value assigned over#
A value assigned over dies as it is replaced: the new value is evaluated first, then the old one is finalized, then the new one is stored, as v = make(5) showed. This is so for a variable, a field or an element of one, and a value reached through a reference (*r = v). Where the variable holds nothing to replace, because it was never given a value or its value moved out, nothing is finalized; where that depends on the path, its drop flag decides.
A value in a region#
A value allocated in a region is finalized when the region lets it go: when its single owner goes, or, for a counted region, its last owner (When a value is freed). It dies as above, and only then is its memory freed, so a finalizer may still use everything its value owns. A value moved out of a region's object belongs to its new holder, which finalizes it; the object's memory is freed without finalizing what was moved out. Only the whole value moves, never one of its fields (Moving a value out), so no object dies partly moved out.
An enum's value#
An enum's value is finalized as the variant it holds would be: the tag says which. An enum may declare its own final, which runs for every variant, and a variant may declare one too: the variant's finalizer runs first, then the enum's, then the finalizers of the variant's fields (the enum's common fields among them), then the owning references those fields hold are released: the more specific type before the more general. An enum none of whose variants has anything to finalize costs nothing when its value dies: dropping an Option[i32] does no work, while dropping an Option[String] finalizes the string when it holds one, and the owner held in a Some is let go with its Option:
struct Handle { pub fd i32; }
struct Named { pub n i32; fn final(self &uni) { print <- "field final\n"; } }
enum Resource {
fn final(self &uni) { print <- "released\n"; } // runs for every variant, after its own
struct File {
pub handle Handle;
pub name Named;
fn final(self &uni) { print <- "close ", handle.fd, "\n"; } // runs first, for a File
}
struct Nothing {}
}
let r Resource = Resource.File(new Handle(7), new Named(1)); // dies: close 7, released, field final
let s Resource = Resource.Nothing; // dies: released
A global variable#
A global variable lives until the program shuts down. Whatever it holds is then finalized by its module's finalizer, after the module's own final: a value whose type finalizes, a tuple or an array of them, or an owning reference, which is released (The module's initializer, in Modularity, describes a module's own initializer and finalizer):
mod globals;
import stdio use *;
struct R {
pub n i32;
fn final(self &uni) { print <- "final "; print <- n; print <- "\n"; }
}
let one R;
let pair (R, R);
let many Array[R, 2];
let owned Uni[R];
fn @initpure init() {
one = new R(1);
pair = (new R(2), new R(3));
many = [new R(4), new R(5)];
owned = new Uni[R](6);
}
fn main() {
print <- "main ends\n";
}
This prints main ends, and then, after main returns, final 1 through final 6, one to a line.
You can now say, for any value, when it dies and in what order its finalizers run, which is what code holding a socket, a file or a lock needs to know. The single owner of the last page frees its value the moment that owner goes; next is the region that lets several owners hold one value, and frees it when the last of them goes: reference counting.