Pointers
A raw pointer holds the address of a value. *T is a read-only pointer: the value it points at can be read through it. *var T is a writable pointer: the value can also be written. A pointer may be null, may be stored in a field, returned, offset and compared, and may outlive what it points at — the compiler checks only what the pointer type allows, never whether the address is still good. That is the price of the things only a pointer can do: foreign functions, manual allocation, stored links between values, and walking memory by address.
pointer-type = "*" type
| "*" "var" type
address-of = "@" place
dereference = "*" expression
var score = 10;
let writer = @score; // *var int: score is a var
*writer += 5; // score is now 15
let reader: *int = @score; // a read-only view of the same int
let current = *reader; // 15
For safe, checked access to a value someone else owns, use a reference instead; a pointer is the deliberate step outside those checks.
Taking an address
The prefix @ takes the address of a place: a variable, a field, an element, or the target of another pointer. @x.field and @a[i] address one part of a value. The pointer's writability comes from the place:
| Place | @place has type |
|---|---|
a var binding, or a part of one | *var T |
a let binding, or a part of one | *T |
an element of a var T[..] view | *var T |
an element of a T[..] view | *T |
a field or element reached through a *var T | *var T |
error: cannot assign '*int' to '*var int': '@limit' yields a read-only '*T'; declare 'limit' with 'var' for a '*var T'
& never takes an address in Rux: &x is rejected with '&' does not take an address; write '@' to take the address of a value. A pointer is made only from an address, so a plain value where a pointer is expected is the error cannot assign 'int' to '*int'.
Taking the writable address of a variable declared without a value counts as initialising it, because the address exists to be filled through: var info: SystemInfo; GetSystemInfo(@info); leaves info usable. See Initialisation.
Reading and writing through a pointer
The prefix * dereferences a pointer: *p is the value it points at, a place that can be read, and written when p is a *var T:
func Store(target: *var int, value: int) {
*target = value;
}
error: cannot modify data through read-only pointer '*int'
The pointee type, not the variable it points at, decides what is allowed: a *int cannot write even when it points at a var.
A write through a pointer initialises the storage it addresses and destroys nothing. The pointer cannot tell whether that storage holds a value yet, so *p = value, *p <- value, p[i] = value and a field write through a *var T never run a destructor for what was there before. Code that replaces a value through a pointer destroys or moves out the old one first:
struct Noisy {
id: int;
}
extend Noisy {
func ~Noisy(self: &var Noisy) {
PrintLine("destroy {}", self.id);
}
}
func Main() -> int {
var slot = Noisy { id: 1 };
slot = Noisy { id: 2 }; // assignment: destroys 1
let p = @slot;
*p = Noisy { id: 3 }; // through a pointer: 2 is never destroyed
return 0; // destroys 3
}
destroy 1
destroy 3
Moving a value out through a pointer is rejected, because the pointer does not own it: let mine <- *p; fails with cannot move '*p' out of borrowed pointer storage.
Fields, elements and methods
. reaches through a pointer to a struct, and [i] through a pointer to an element; there is no arrow operator. Reads and writes go straight to the pointed-at value:
struct Point {
x: int;
y: int;
}
func Main() -> int {
var point = Point { x: 1, y: 2 };
let p = @point;
p.x = 7; // point.x is now 7
let sum = p.x + p.y;
return 0;
}
A method is not called through a raw pointer, because its receiver is a reference and a pointer proves nothing about the address it holds. Dereference first, (*p).Shift(3), to vouch for it explicitly:
error: cannot create safe receiver '&var Point' from raw pointer '*var Point'
note: raw pointers do not prove a valid non-null borrow
help: call the method on an owning value
Pointers nest: **T is a pointer to a pointer, and each * removes one level, so with pp: **var int, **pp = 1 writes the int.
Conversions
| From | To | How |
|---|---|---|
*var T | *T | implicitly |
*T | *opaque | implicitly; *var T likewise to *var opaque |
*opaque | *T | as *T; likewise as *var T |
*T | *U | as *U |
*T | *var U | never, for any U but opaque |
| a pointer | uint | as uint — the address as a number |
uint | a pointer | as *T |
Losing write access is always allowed; gaining it is not: cannot assign '*int' to '*var int'. A cast with as is the explicit, unchecked way to retype an address — see Casts — but it cannot add write access either, whatever pointee it names, so @x as *var int and @x as *var uint64 on a let x are refused. Untyped memory is the one way through: writable memory reached through an *opaque, such as a fresh allocation, is retyped with as *var T, and a *T may become a *var opaque for a C function that takes a void * it only reads:
error: cannot cast '*int' to '*var uint64', which would give a read-only pointer write access
null
null is the pointer that points at nothing. It converts to every pointer type, and it is the only value a pointer has that is not an address. Compare with == and != before dereferencing a pointer that might be null:
struct Node {
value: int;
next: *Node;
}
func Main() -> int {
var tail = Node { value: 3, next: null };
var head = Node { value: 1, next: @tail };
var cursor: *Node = @head;
while cursor != null {
PrintLine("{}", cursor.value);
cursor = cursor.next;
}
return 0;
}
Reading or writing through null is not checked: it compiles, and crashes the program when it runs.
null is a pointer value, not absence. It is not a value of an optional type, and where an optional of a pointer is expected it is a present null pointer:
| Type | Meaning |
|---|---|
(*T)? | an optional pointer: none, or a pointer |
*T? | a pointer to an optional, *(T?) |
let present: (*int)? = null; // present, holding a null pointer
let absent: (*int)? = none; // absent
*opaque
*opaque and *var opaque point at bytes of no particular type, as C's void * does. Allocation functions return them, and foreign functions take and return them. Any typed pointer converts to one implicitly; to use the memory, cast it back to a typed pointer:
import Memory::{ Alloc, Free };
func Main() -> int {
let block = Alloc(4 * sizeof(int)) as *var int;
if block == null {
return 1;
}
defer Free(block);
block[0] = 1;
return 0;
}
Arithmetic on an *opaque moves by single bytes.
Comparison
Two pointers compare with == and !=, which compare addresses, and with <, <=, > and >=, which order them — useful when walking a buffer up to an end pointer. See Pointer arithmetic.
What a pointer does not have
A pointer has no length: p.length is type '*var int' has no field 'length'. It cannot be iterated with for, and slicing it needs an explicit end. To treat raw memory as a sequence, build a view of it with a count, p[..n], and iterate that, for value in p[..n]:
error: cannot iterate over pointer '*int32'
help: iterate a slice of the memory it points at, as in 'for value in p[..count]'
A pointer is 8 bytes on every supported target, whatever it points at.
See also
- Pointer arithmetic — moving a pointer by whole elements
- Pointer slicing — a bounded view of the memory a pointer addresses
- References — checked, non-null access to a value
- Foreign functions — how pointers cross into C
- Layout — the size and alignment of what a pointer addresses
- Learn: Pointer, Out parameter, Raw memory, Optional pointer