Memory · Lesson 15.10

Box

Source
Own one allocated value with Box<T>, which destroys the value and returns its memory when the box's life ends.

The raw lessons kept leaving one job to you: remember to give the memory back, exactly once, and never touch it afterwards. A Box<T> takes that job over. It owns one value that lives in allocated memory, and — like any value with a destructor — it cleans up after itself when its life ends.

Creating a box

Box::Create<T>(allocator, value) asks the allocator for room, moves the value in, and hands back the box:

let boxed = Box::Create<Tally>(allocator, Tally { name: name, count: 1 })?;

Asking can fail, so Create returns Box<T> ! AllocError, and ? passes a refusal on. From this line on, the box is the value's one owner.

Reaching the value

Get() returns a *var T into the box's storage, so fields are reached through it with a plain ., as through any pointer:

apples.Get().count += 2;
PrintLine("    {} {}", apples.Get().count, apples.Get().name);

That pointer does not own anything. It is valid only while the box is alive — the box decides when the storage goes, not the pointer.

The box cleans up

Nobody calls Deallocate in this program. When a box's life ends, its destructor destroys the value — which runs ~Tally and prints a line — and then gives the memory back to the allocator it came from:

flowchart LR
    create["Box::Create(allocator, value)"] --> alloc["the allocator gives room,<br/>the value moves in"]
    alloc --> use["boxed.Get() — use it"]
    use --> end_["the box's life ends"]
    end_ --> drop["~Tally runs"]
    drop --> back["memory goes back to<br/>the same allocator"]

You can see the timing in the output. The box in Count dies when Count returns, so destroying the pears tally is printed before Main carries on with a box in Main:.

Moving a box

A box cannot be copied: two owners would both release the memory, and the second release would hit memory that was no longer theirs. It can be moved with <-, which hands the ownership on and retires the old name:

let storage = apples.Get();
let owner <- apples;
PrintLine("    moved, same storage: {}", owner.Get() == storage);

The value itself stays where it was allocated, so the new owner points at the very same storage — the comparison prints true. Only the right to release it has moved. At the end of Main, owner releases the memory, once, and apples releases nothing because it no longer owns anything.

You writeWhat happens
let owner <- apples;ownership moves; apples can no longer be used
let owner = apples;rejected — a box cannot be copied
apples.Get()a borrowed *var T, valid while the box lives

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// A `Box<T>` owns one value that lives in allocated memory. `Box::Create<T>(allocator, value)`
// asks the allocator for room, moves the value in, and hands back the box, or an `AllocError`
// if there was no room. From then on the box is the value's one owner.
//
// That ownership is what the raw lessons were missing. Nobody calls `Deallocate`: when the box's
// life ends, its destructor destroys the value and gives the memory back to the allocator it
// came from. And a box cannot be copied, because two owners would release the memory twice.
// Hand it on with `<-`, and the old binding can no longer be used.
//
// `Get()` returns a `*var T` into the box's storage. That pointer does not own anything: it is
// valid only while the box is alive.
import Allocator::{ AllocError, Allocator, Box, SystemAllocator };
import Io::PrintLine;

struct Tally {
    name: char8[..];
    count: int;
}

extend Tally {
    func ~Tally(self: &var Tally) {
        PrintLine("    destroying the {} tally", self.name);
    }
}

// The box is created and dies inside this call, so its memory is returned before `Main` resumes.
func Count(allocator: Allocator, name: char8[..]) -> ! AllocError {
    let boxed = Box::Create<Tally>(allocator, Tally { name: name, count: 1 })?;
    PrintLine("    counted {} {}", boxed.Get().count, boxed.Get().name);
}

func Main() -> ! AllocError {
    var system = SystemAllocator();
    let allocator: Allocator = system;

    PrintLine("a short-lived box:");
    Count(allocator, "pears")?;

    PrintLine("a box in Main:");
    var apples = Box::Create<Tally>(allocator, Tally { name: "apples", count: 3 })?;
    apples.Get().count += 2;
    PrintLine("    {} {}", apples.Get().count, apples.Get().name);

    // Moving the box moves the ownership. The value itself stays where it was allocated, so the
    // new owner points at the very same storage.
    let storage = apples.Get();
    let owner <- apples;
    PrintLine("    moved, same storage: {}", owner.Get() == storage);

    // Using `apples` from here on is rejected: it was moved out. `owner` releases the memory, once.
    PrintLine("end of Main:");
}

Besides Io, its Rux.toml lists Allocator under [Dependencies].

Run it

cd Examples/Memory/Box
rux run
a short-lived box:
    counted 1 pears
    destroying the pears tally
a box in Main:
    5 apples
    moved, same storage: true
end of Main:
    destroying the apples tally

Common mistakes

Copying a box.
let owner = apples; fails with error: move-only value 'apples' requires an explicit '<-' in initialization, and the help line suggests let destination <- apples. Move it, or keep using the one box.
Using a box after moving it.
After let owner <- apples;, any use of apples fails with error: value 'apples' is used after it was moved. Use owner from then on.
Forgetting the ?.
let apples = Box::Create<Tally>(…); without ? holds a fallible, so apples.Get() fails with error: type 'Box<Tally> ! AllocError' has no field 'Get'. Handle the AllocError first.
Keeping the pointer from Get() longer than the box.
A function that creates a box and returns boxed.Get() compiles — but the box dies on the way out, and the caller is left holding the address of released memory. Return the box itself, moved with <-, instead.

Try it yourself

  1. Create a second box, plums, in Main before apples. Predict the order of the two destroying lines at the end, then run.
  2. Write func Report(owner: Box<Tally>) that prints the tally, and call it as Report(<-apples). Where does destroying the apples tally appear now?
  3. Change Count to return the box, -> Box<Tally> ! AllocError, with return <-boxed;, and keep it alive in Main.

Learn more

  • Move and Destructor — the ownership rules a box is built on
  • Allocator — where a box's memory comes from
  • Arena — an allocator a box can draw from, with one extra rule