Ownership
Ownership is the part of C+ that differs most from C. There is no &T and no &mut T. Borrowing is expressed by parameter markers, not by reference types. The call site carries no markers at all; the function signature is the single place that describes the data flow.
The parameter forms
Non-Copy values move by default. borrow is the opt-out that says "the caller keeps ownership".
| Form | On non-Copy types | On Copy types |
|---|---|---|
x: T |
Move (caller can't use the value after the call) | Pass-by-value copy |
mut x: T |
Exclusive borrow (function may mutate; mutations propagate back) | Pass-by-value, locally mutable |
move x: T |
Move (explicit; same as x: T) |
Pass-by-value |
borrow x: T |
Shared borrow (caller keeps ownership, function reads only) | Redundant on Copy |
Method receivers mirror these, with one deliberate difference in the default:
| Receiver | Meaning |
|---|---|
self |
Shared borrow: read-only access, caller keeps ownership |
mut self |
Exclusive borrow: may mutate, mutations propagate back |
move self |
Move: consumes the receiver, caller can't use it after |
There is no borrow self; bare self already is the shared borrow. The asymmetry (a bare self reads, but a bare x: T parameter moves) is intentional: each defaults to its common case. Most method calls only want to look at the receiver, and most function calls hand a value over to the callee. When you want the other behaviour you say so, and the marker is always visible in the signature.
Copy is structural
A type is Copy if every component is. Primitives and plain enums are Copy. A struct of Copy fields is Copy automatically. A struct that defines fn drop(mut self) is forced to be non-Copy, because silently bit-copying something that owns a resource would lead to a double free.
struct Point { x: i32, y: i32 } // Copy (all fields Copy)
struct Buf { ptr: *u8, len: usize }
impl Buf {
fn drop(mut self) { unsafe { free(self.ptr); } } // forces non-Copy
}
Return values always move:
fn make_buf() -> Buf { ... } // no marker; returning is always a move
When to use borrow
Since non-Copy params move by default, the question flips: when does the callee not need to consume the value? Use borrow:
// Default: x moves in. Caller can't use `s` after the call.
fn echo(x: string) -> string { return x; }
// Caller keeps `s`; callee only reads. To return a string it makes its own,
// typically via .clone().
fn label(borrow x: string) -> string { return x.clone(); }
let s: string = "hello".to_string();
let r: string = label(s); // s still usable after this call
When the compiler sees a borrow-shaped use (read-only, no consume) inside a default-move body, it suggests borrow with a precise fix-it (E0902).
restrict: opt-in noalias for raw pointers
The borrow checker does not reason about *T raw pointers, so by default LLVM must assume any two pointer arguments may alias. For numeric hot paths that is a real tax: the autovectorizer inserts a runtime alias check and a scalar fallback. The restrict parameter marker asserts that the pointer does not alias any other pointer reachable in the body, and lowers to LLVM noalias.
fn axpy(n: usize, a: f32, restrict x: *f32, restrict y: *f32) {
let mut i: usize = 0 as usize;
while i < n {
unsafe { y[i] = a * x[i] + y[i]; }
i = i +% (1 as usize);
}
return;
}
restrict is only valid on *T params (other shapes fire E0411), needs no unsafe at the declaration, composes with mut, and is C ABI compatible (a pub extern fn exports the same C signature with or without it).
What the compiler checks, and what it trusts
C+ ownership is boundary-checked, not whole-program inferred. It is worth knowing exactly where a rule is enforced and where you are trusted.
Enforced by the compiler:
- Use after move: reading a value after it moves is E0335.
- Aliasing XOR mutation: a place has shared borrows or one exclusive borrow, never both (see the borrow checker).
- Partial move out of a
Droptype: rejected (E0509), since the destructor frees fields by hand and stealing one would double free. - Returned borrows: a
str/T[]/borrow REGIONresult must come from a parameter or'staticdata, never from a local that drops at return (E0513).
Trusted to you (the escape hatches):
- A
str/T[]view stored into a longer-lived place. These areCopyviews, not tracked references. The compiler checks the function boundary, but once you copy a view into a field or another binding it no longer tracks that the backing storage outlives it. - Raw pointers (
*T). Completely outside the borrow checker. Theunsafeyou write at each dereference is where you take on the validity obligation.
One rule covers all of it: a borrow, a view, or a raw pointer must not outlive the value it points into. The compiler proves this at the enforced cases; everywhere else it is a contract you keep, and unsafe marks where you signed up for it.
Next
Continue with the borrow checker.