Rc
A Box<T> knows when to free its value because it is the value's only owner. Some values have no single owner. An editor's colour theme is used by every open window; a loaded image may be shown in three places. The memory should go when the last user is done with it — and no one user knows whether it is the last.
Rc<T> — a reference-counted handle — answers that by counting:
import Allocator::{ AllocError, Allocator, Rc, SystemAllocator };
Creating and copying handles
Rc::Create<T>(allocator, value) moves the value into allocated memory beside a count, which starts at one, and returns the first handle. Like Box::Create, it can fail, so it returns Rc<T> ! AllocError:
var theme = Rc::Create<Theme>(allocator, Theme { name: "paper", size: 11 })?;
Copying a handle does not copy the value. Rc has a custom copy operator that adds one to the count, and the new handle reaches the same value. So every window below holds a handle to the theme, not a theme of its own:
struct Window {
title: char8[..];
theme: Rc<Theme>;
}
flowchart LR
m["`theme` in `Main`"] --> v["the paper theme<br/>count: 4"]
p["`theme` parameter"] --> v
e["`editor.theme`"] --> v
w["`preview.theme`"] --> vCount() reads the count. Passing a handle by value — or a struct that holds one — copies it too, which is why Draw sees five handles while the call lasts.
The last handle frees the value
When a handle's life ends, the count goes down by one. The handle that takes it to zero destroys the value — running its destructor — and gives the memory back. In the program, every handle made in OpenWindows ends with the call, and the count returns to one:
| Moment | Handles to the paper theme |
|---|---|
after Rc::Create | 1 |
inside OpenWindows, two windows made | 4 |
inside Draw | 5 |
OpenWindows returned | 1 |
Main ends | 0 — ~Theme runs |
Nothing in the program says when to free the theme. the paper theme is destroyed is printed after end of Main: because Main's handle was the last.
Reading and writing
Get() returns a *T, a read-only pointer, valid while any handle exists. A shared value is read by everybody, and a change made through one handle would surprise all the others, so there is no way to write through Get().
GetMutable() returns (*var T)? — a writable pointer while this handle is the only one, and none while the value is shared:
match theme.GetMutable() {
value? => value.size = 12,
none => PrintLine("shared, so read-only")
}
Right after Create there is one handle, so the program may set the size. Inside OpenWindows the same call answers none. A value that must change while it is shared needs a different design: build a new value and replace the handle, or keep the changing part outside the shared one.
Same value, or equal value
SameAs asks whether two handles reach the same value, not whether two values are equal:
let lookalike = Rc::Create<Theme>(allocator, Theme { name: "lookalike", size: 12 })?;
PrintLine(" editor and preview share: {}", editor.theme.SameAs(preview.theme));
PrintLine(" lookalike is shared: {}", lookalike.SameAs(theme));
The lookalike is a separate value with its own count of one, so it is destroyed as soon as its only handle goes, at the end of OpenWindows.
An
Rc keeps its count with plain arithmetic, which is fast but not safe when two threads change it at once. To share a value between threads, the Allocator package has a twin, Arc<T>, which counts atomically. It comes with Arc in Part 25.The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A `Box<T>` has exactly one owner, which is what lets it know when to free its value. Some values
// have no single owner, though: one theme used by every window of an editor, one loaded image
// shown in several places. Whichever user goes last should free it, and none of them knows
// which one that is.
//
// `Rc<T>`, a reference-counted handle, settles it by counting. `Rc::Create` moves the value into
// allocated memory with a count of one. Copying a handle does not copy the value: it adds one
// to the count, and both handles reach the same value. When a handle's life ends the count goes
// down by one, and the handle that takes it to zero destroys the value and frees the memory.
//
// Shared values are read-only. `Get()` returns a `*T` for reading. `GetMutable()` returns a
// `*var T` only while a single handle exists, since a change made through one handle would
// otherwise surprise the others; while the value is shared it returns `none`.
//
// An `Rc` counts with plain arithmetic, so its handles must all stay on one thread. Its twin,
// `Arc`, counts atomically for values shared between threads; it comes with the Threads part.
import Allocator::{ AllocError, Allocator, Rc, SystemAllocator };
import Io::PrintLine;
struct Theme {
name: char8[..];
size: int32;
}
extend Theme {
func ~Theme(self: &var Theme) {
PrintLine(" the {} theme is destroyed", self.name);
}
}
// Every window holds a handle to its theme, not a copy of it.
struct Window {
title: char8[..];
theme: Rc<Theme>;
}
// The window arrives by value, so for the length of the call there is one more handle.
func Draw(window: Window) {
let theme = window.theme.Get();
PrintLine(" {} in {} at {}pt, {} handles", window.title, theme.name, theme.size,
window.theme.Count());
}
func OpenWindows(theme: Rc<Theme>, allocator: Allocator) -> ! AllocError {
// Two more handles: the count is now one for `Main`, one for this parameter and one each
// for the windows.
let editor = Window { title: "editor", theme: theme };
let preview = Window { title: "preview", theme: theme };
PrintLine(" {} handles", theme.Count());
Draw(editor);
// `GetMutable` needs a `var` handle, so take one more copy. Even so, the answer is `none`:
// while the value is shared, nobody may write to it.
var mine = theme;
PrintLine(" can write: {}", mine.GetMutable() != none);
// `SameAs` asks whether two handles reach one value, not whether two values are equal. A
// second theme with the same fields is still a different value, with its own count.
let lookalike = Rc::Create<Theme>(allocator, Theme { name: "lookalike", size: 12 })?;
PrintLine(" editor and preview share: {}", editor.theme.SameAs(preview.theme));
PrintLine(" lookalike is shared: {}", lookalike.SameAs(theme));
// Every handle made here ends with the call. The lookalike was the only handle to its
// value, so that value is destroyed now; the paper theme still has `Main`'s handle.
}
func Main() -> ! AllocError {
var system = SystemAllocator();
let allocator: Allocator = system;
var theme = Rc::Create<Theme>(allocator, Theme { name: "paper", size: 11 })?;
PrintLine("created, {} handle", theme.Count());
// A lone handle may write. `GetMutable` returns `(*var Theme)?`, a pointer or `none`.
match theme.GetMutable() {
value? => value.size = 12,
none => PrintLine("shared, so read-only")
}
// `Get()` gives only a `*Theme`, so writing through it is refused:
//
// error: cannot modify data through read-only pointer '*Theme'
PrintLine("opening windows:");
OpenWindows(theme, allocator)?;
PrintLine("windows closed, {} handle", theme.Count());
// `theme` is the last handle, so the value is destroyed when `Main` ends, and only then.
PrintLine("end of Main:");
}
Besides Io, its Rux.toml lists Allocator under [Dependencies].
Run it
cd Examples/Memory/Rc
rux run
created, 1 handle
opening windows:
4 handles
editor in paper at 12pt, 5 handles
can write: false
editor and preview share: true
lookalike is shared: false
the lookalike theme is destroyed
windows closed, 1 handle
end of Main:
the paper theme is destroyed
Common mistakes
Try it yourself
- Print
theme.Count()insideDrawbefore and afterlet copy = window.theme;. How much does one moreletadd? - In
Main, afterOpenWindows, tryGetMutableagain and set the size to 14. Does it work now, and why? - Make the lookalike a second handle to
themeinstead of a new value. What doesSameAssay, and what happens to thethe lookalike theme is destroyedline? - Give
Windowa destructor that prints its title, and watch the order in which windows and themes go.
Learn more
- Box — one owner, no counting
- Custom copy — the copy operator that does the counting
- Arc — the same handle, safe to share between threads
- Allocator in the API reference
- Copy and move and Destructors in the Rux Reference
15.14 Zeroize
Wipe a secret with Zeroize, a clear that the compiler promises never to optimize away.
Overview
Numbers in depth: integers past 64 bits, the limits of every type, infinity and NaN, working with bits, overflow you can detect or plan for, byte order, and the Math package with its complex numbers.