Tutorial: Pointers
This tutorial introduces Metel's regular pointer model. The core idea is simple:
&xtakes the address of a value and produces*T&mut xtakes a mutable address and produces*mut T&mutalso works on addressable fields, elements, and nested lvalue paths- pointers are storable and cloneable handles
Taking an address
The syntax uses & as address-of:
fun main() {
let mut value = 42;
let read_ptr: *i64 = &value;
let write_ptr: *mut i64 = &mut value;
}
*i64 is a pointer to i64. *mut i64 is a mutable pointer to i64.
Read This As Two Choices
Choose pointer mutability at the address-taking site:
&xfor*T&mut xfor*mut T
Why pointers exist
Pointers are mainly about shared state and indirection. They let multiple places talk about the same value instead of copying it around.
That matters in patterns like:
- graph nodes with back-links
- mutable shared state between closures
- data structures where values point at each other
Shared mutable state
Pointers are useful when multiple parts of a program need to talk about the same value explicitly. That includes shared mutable state between closures:
fun main() {
let mut total = 0;
let cell: *mut i64 = &mut total;
let add_one = () -> {
*cell += 1;
};
let add_ten = () -> {
*cell += 10;
};
add_one();
add_ten();
println(total); // 11
}
Both closures mutate the same underlying i64 through the same pointer.
Ordinary closure capture is by value. The pointer is what makes the sharing explicit: both closures talk to the same storage location, not independent copies.
Taking mutable addresses of fields and elements
In v0.8.0, &mut is not limited to a plain variable. It also works on addressable paths such as struct fields, tuple elements, and array elements:
struct Counter {
value: i64,
}
fun main() {
let mut counter = Counter { value: 10 };
let field_ptr: *mut i64 = &mut counter.value;
*field_ptr += 5;
println(counter.value); // 15
}
That same rule applies to nested paths. The write goes back to the original storage location, not a temporary copy.
Dereferencing
Use *p to read through a pointer, and *p = value to write through a mutable pointer:
fun main() {
let mut n = 1;
let p: *mut i64 = &mut n;
*p = 4;
println(*p); // 4
}
*T is read-only. *mut T is readable and writable.
Explicit Where It Matters
Metel keeps pointer reads and writes explicit. You can spot aliasing-sensitive operations by looking for &, &mut, or *.
Auto-deref for fields and methods
Field access and method calls automatically dereference one pointer layer:
struct Counter {
value: i64,
}
impl Counter {
fun increment(&mut self) {
self.value += 1;
}
}
fun main() {
let mut counter = Counter { value: 0 };
let p: *mut Counter = &mut counter;
p.increment();
println(p.value); // 1
}
This is shorthand for (*p).increment() and (*p).value.
Auto-deref is only for field access, method calls, and function-pointer calls. Plain reads, writes, and argument passing still use explicit *p.
Function pointers
Pointers to closures or functions can be called directly:
fun main() {
let f = () -> i64 { return 42; };
let ptr: *() -> i64 = &f;
println(ptr()); // 42
}
Pointer types and linear values
Regular pointers provide explicit aliasing for non-linear values:
*Tand*mut Tare for non-linear, shared values- linear values are not meant to be addressed with
&x
This keeps pointer aliasing and linear ownership from fighting each other.
Boundary Rule
Regular pointers are for explicit aliasing of non-linear values. If a feature depends on exactly-once ownership, regular *T / *mut T are the wrong tool.
What you learned
&xyields*T.&mut xyields*mut T, and&mutalso works on addressable lvalue paths.*preads and*p = valuewrites through a pointer.- Fields, methods, and function-pointer calls auto-dereference one pointer layer.
- Pointers are for explicit aliasing of non-linear values.
Next: Closures and Capturing — first-class functions, captured values, and shared mutation.