C+
Ownership & safety · v0.0.13

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 Drop type: rejected (E0509), since the destructor frees fields by hand and stealing one would double free.
  • Returned borrows: a str / T[] / borrow REGION result must come from a parameter or 'static data, 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 are Copy views, 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. The unsafe you 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.