References and pointers
Volt has two kinds of address:
T& reference |
T* pointer |
|
|---|---|---|
| null | never | may be |
| optional | can’t be (T&? is an error) |
is its own “maybe” |
| reaching through | . |
-> |
arithmetic, p[i] |
no | yes, unchecked, like C |
| owns | never | never |
&x gives a T&. A T& converts to a T* by itself; a T* becomes a T& through if (p) or
p ?? other, which check for null.
References
Section titled “References”use std::io;
struct account { balance: i32;}
fn deposit(a: account&, amount: i32) -> void { a.balance += amount; // `.` reaches through the reference}
fn main() -> void { var acct: account = { balance: 10 }; deposit(&acct, 5); val r = &acct.balance; std::println("{} {}", acct.balance, *r);}// expect: 15 15*r reads through a reference; printing r itself prints the address.
References to a val
Section titled “References to a val”A reference to a val (or to a parameter without var) can be read through, never written
through. Passing &x, or calling a method on x, is fine as long as what’s called only reads:
use std::io;
struct account { balance: i32;}
attach fn report(this: account&) -> i32 { return this.balance;}
attach fn deposit(this: account&, amount: i32) -> void { this.balance += amount;}
fn main() -> void { val fixed: account = { balance: 10 }; var open: account = { balance: 10 }; open.deposit(5); std::println("{} {}", fixed.report(), open.report());}// expect: 10 15Calling deposit on the val is an error, found where the call is:
struct account { balance: i32;}
attach fn deposit(this: account&, amount: i32) -> void { this.balance += amount;}
fn main() -> void { val fixed: account = { balance: 10 }; fixed.deposit(5);}// error: 'fixed' is a val, and deposit changes it (through this): declare it with varThe compiler follows the reference through every call: a function that passes it on to one that
writes through it changes it too, and so do closures and fn values that do. Writing through a
reference made from a val (val r = &x; *r = 1;) is an error right away, and so is sorting a
slice of a val array. A reference a function returns points where its argument did, so writing
through what x_of(&p) returns changes p:
struct point { x: i32;}
attach fn x_of(this: point&) -> i32& { return &this.x;}
fn main() -> void { val p: point = { x: 0 }; *p.x_of() = 1;}// error: 'p' is a val, and it's changed through what x_of returns: declare it with varA function can return a reference to what its caller lent it, but not to its own locals or the
parameters it took by value: they’re gone once it returns. Returning &x, a field or element of
x, or a struct, tuple or array holding one, is an error:
fn biggest() -> i32& { var best = 0; return &best;}
fn main() -> void { val b = biggest();}// error: can't return a reference to 'best'Returning a local that holds a reference to one of the function’s own locals (a struct built with
{ x: &n }, a pointer) builds, with a warning;
so does reading a view into a container (xs.items()) after a call that may move its storage.
A reference keeps where it points when it’s stored in a struct, tuple or array, passed or returned inside one (or inside an error union), or assigned to a variable later:
struct holder { r: i32&;}
fn main() -> void { val x = 0; val h: holder = { r: &x }; *h.r = 1;}// error: can't assign through this; it reaches a val (or a parameter without var)The references in one value share a verdict: when one of them points at a val, writing through
any of them is an error.
Calls into C, pointers made with @cast, ones an await gives, a reference stored through another
reference, and references kept in an enum’s payload or in a struct that also owns memory through a
pointer (as a std::vec does) aren’t checked.
A val is shallow: what its pointers and slices point at isn’t part of it. A val slice of a var
array can still change the array’s elements; a slice of a val array can’t.
Pointers
Section titled “Pointers”Pointers are for C interop and low-level code: they can be null, do arithmetic (p + n, p - q,
p++), and index without bounds checks. -> reaches a field through one. Dereferencing a null
pointer stops the program in debug builds.
use std::io;
struct node { key: i32; next: node*; // defaults to null}
fn find(var cur: node*, key: i32) -> node* { while (cur) { // narrows: cur is a node& inside if (cur.key == key) { return cur; } cur = cur->next; } return null;}
fn main() -> void { var c: node = { key: 3 }; var b: node = { key: 2, next: &c }; var a: node = { key: 1, next: &b }; val hit = find(&a, 3); val miss = find(&a, 9); std::println("{} {}", hit->key, miss == null); var nums: i32[] = { 10, 20, 30 }; val p: i32* = &nums[0]; std::println("{} {}", p[2], *(p + 1));}// expect: 3 true// expect: 30 20void* is C’s opaque pointer: @cast it to a real pointer type before using it.
What references can’t do
Section titled “What references can’t do”A reference can’t be null, so it can’t be optional either, and a reference field doesn’t default to
null as a pointer field does. Store a T* when “none” has to be possible:
fn main() -> void { val x: i32&? = null;}// error: optional