SuperAPI AI Directory

rust-syntax-ownership

Coding & development

View repository

Updated 10-15 22:18

About this skill

rust-syntax-ownership

The mechanics of ownership transfer in Rust: how moves work, when Copy applies, when to call .clone(), when Drop runs, and the API patterns that consume self by value (builder, From/Into, into_inner). Partial moves out of struct fields are covered here too.

Cross-references: [[rust-core-memory-model]] (conceptual overview, big picture) [[rust-syntax-borrowing]] (the alternative to move) [[rust-errors-borrow-checker]] (E-codes in depth)


When to use this skill

  • User writes code that hits a move-related compile error: E0382, E0507, E0509, E0040, E0184, E0204, E0515
  • User asks "why was this value moved" or "why can't I use x after passing it to a function"
  • User wonders whether to derive Copy, derive Clone, both, or neither
  • User wants to design a fluent builder, an Into<T> conversion, or an into_inner accessor
  • User struggles with partial moves out of struct fields (s.field moved, but s still partly usable)
  • User asks "do I need to clone here" or wants to remove .clone() calls that Clippy flagged

For the borrow alternative (sharing without taking ownership) see [[rust-syntax-borrowing]]. For the conceptual story (stack vs heap, smart pointers, Send/Sync) see [[rust-core-memory-model]].


The three ownership rules (verbatim from the Book)

  1. Each value in Rust has an owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

ALWAYS state these three rules first when explaining any ownership scenario. Every other rule in this skill is a consequence of them.

Source: Rust Book Ch 4.1 - What is Ownership?


Quick reference table

Situation What happens Compiler behaviour
let b = a; and a: String a is moved into b Using a afterwards: E0382
let b = a; and a: i32 a is copied into b Both a and b remain valid
fn take(s: String) called with take(my_string) my_string moved into the function Caller's my_string invalidated
fn take(s: String) -> String { s } Ownership returned to caller Receiver of return value owns it
let s = my_struct.field; (non-Copy field) Partial move out of my_struct my_struct cannot be used as a whole; other fields still usable
drop(value) Calls Drop::drop immediately, then frees value invalidated after the call
value.drop() NEVER allowed; method is private E0040
Owner goes out of scope Drop::drop runs, then memory is freed Automatic, no syntax

Move semantics

Assigning a non-Copy value moves ownership. The original binding is invalidated at compile time:

let s1 = String::from("hi");
let s2 = s1;                 // s1 moved into s2
// println!("{s1}");         // E0382: value used after move
println!("{s2}");            // ok

The same rule applies to:

  • Function arguments: fn take(s: String) consumes the caller's value.
  • Return values: fn give() -> String { String::from("hi") } moves the value to the caller.
  • Destructuring: let (a, b) = pair; where pair: (String, String) moves both halves; pair is invalidated.
  • Closure captures: move || println!("{s}") moves s into the closure.

ALWAYS think of a binding as the owner, not as the storage cell. The value is moved; the heap payload (if any) is now reachable only through the new owner.

NEVER expect a moved binding to be usable. The compiler tracks moves statically; even an if-only move makes the binding unconditionally invalid on the other path unless the value is re-initialised.

Source: Rust Book Ch 4.1 - What is Ownership?


The Copy trait

Copy makes assignment a bit-by-bit duplication rather than a move. Copy is implicit, never overloadable:

let x: i32 = 5;
let y = x;            // copy (i32 is Copy)
println!("{x} {y}");  // both valid

Rules for Copy

  • A type can implement Copy only if all its fields are Copy.
  • Copy and Drop are mutually exclusive. A type that implements Drop can never be Copy (E0184).
  • Copy requires Clone. Every Copy type must also derive or implement Clone (E0204 if missing).
  • #[derive(Copy, Clone)] works only when all fields are themselves Copy. If any field is String, Vec, Box, Rc, Arc, etc., the derive fails.

Which types are Copy

Always Copy Never Copy
All integers: i8 ... u128, isize, usize String (owns heap buffer)
Floats: f32, f64 Vec<T> (owns heap buffer)
bool, char Box<T> (owns heap allocation)
The never type ! Rc<T>, Arc<T> (own refcount)
Shared references &T for any T Anything implementing Drop
Raw pointers *const T, *mut T Tuples / arrays containing non-Copy elements
Function pointers fn(...) -> _ Mutable references &mut T
Function items (each fn's anonymous type) Most user-defined types unless explicitly derived
Tuples / arrays where every element is Copy

ALWAYS check field-by-field before deriving Copy. NEVER derive Copy on a type owning heap data.

Source: std::marker::Copy


The Clone trait

Clone is explicit and arbitrary. Calling .clone() runs user-defined code:

let a = String::from("hi");
let b = a.clone();        // deep copy: heap buffer duplicated
println!("{a} {b}");      // both valid

Rules for Clone

  • Clone is a supertrait of Copy. Every Copy type also implements Clone.
  • For a Copy type, clone() can be (and typically is, when derived) implemented as *self.
  • #[derive(Clone)] requires all fields to be Clone.
  • A type can implement Clone without being Copy (the common case for heap-owning types).

Clone-but-not-Copy types

  • String, Vec<T>, Box<T>, HashMap<K, V>, BTreeMap<K, V>, Rc<T>, Arc<T> are all Clone.
  • Rc::clone(&rc) and Arc::clone(&arc) bump the refcount; they do NOT deep-copy the inner T.
  • Vec<T> requires T: Clone to derive Clone.

ALWAYS prefer Rc::clone(&rc) / Arc::clone(&arc) over rc.clone() in code that should make the cheap refcount-bump intent explicit. Both compile to identical code.

NEVER use .clone() to silence the borrow checker. Borrow first; clone only when you genuinely need two independent owners (Clippy lint: clippy::redundant_clone).

Source: std::clone::Clone


Decision tree: move vs borrow vs clone

Need to pass a value to a function or another binding?
|
+-- Will the original be used again afterwards?
|   |
|   +-- NO  -> move (just pass it; ownership transfers cleanly)
|   |
|   +-- YES -> borrow (& for read, &mut for write)
|              See [[rust-syntax-borrowing]] for details.
|
+-- The borrow checker rejected your borrow attempt?
    |
    +-- Step 1: restructure (scope the borrow shorter, split the data,
    |           return an owned value, take ownership by value).
    +-- Step 2: ONLY clone when:
    |           - The type is cheap to clone (Copy-like, Arc<T>, small Box).
    |           - OR you genuinely need two independent owners.
    +-- NEVER clone "just to make it compile". This is the
        single most common Rust anti-pattern (Clippy: redundant_clone).

ALWAYS try borrowing before cloning. ALWAYS try restructuring before cloning. Cloning is the third choice, not the first.


Drop and the explicit drop() function

Drop runs automatically when an owner goes out of scope. This is Rust's RAII guarantee:

pub trait Drop {
    fn drop(&mut self);
}

Critical rules

  • You cannot call .drop() directly on a value (E0040: "explicit use of destructor method"). The method is implicitly private at the call site.
  • To drop early, use the free function std::mem::drop, which is in the prelude as drop:
    let s = String::from("hi");
    drop(s);             // calls Drop::drop, then `s` is gone
    // println!("{s}");  // E0382: value used after move
    
    drop() is literally just pub fn drop<T>(_x: T) {}. It takes its argument by value, so the move-and-go-out-of-scope rule does the rest.
  • Drop::drop runs even during panic unwinding. This is what makes Mutex poisoning, File close, and MutexGuard release correct.
  • Drop::drop itself SHOULD NOT panic. A panic while unwinding from another panic becomes a double-panic and aborts the process.

Drop order

  • Local variables in a block: dropped in reverse declaration order (LIFO).
  • Struct fields, tuple elements, array elements: dropped in declaration order (first-to-last).
  • Enum variants: the active variant's fields drop in declaration order.
struct Loud(&'static str);
impl Drop for Loud {
    fn drop(&mut self) { println!("drop {}", self.0); }
}

fn demo() {
    let a = Loud("a");   // declared first  -> dropped last
    let b = Loud("b");   // declared second -> dropped first
}
// Output:
// drop b
// drop a

ALWAYS rely on Drop for resource cleanup. NEVER design a type that requires the user to call a manual .close() method.

Source: std::mem::drop Source: std::ops::Drop


Ownership transfer patterns

Pattern A: Builder consuming self

A builder takes self by value, mutates, and returns Self. Each call moves the builder forward:

pub struct Request {
    method: String,
    url: String,
    headers: Vec<(String, String)>,
}

impl Request {
    pub fn new(url: impl Into<String>) -> Self {
        Self { method: "GET".into(), url: url.into(), headers: vec![] }
    }

    pub fn method(mut self, m: impl Into<String>) -> Self {
        self.method = m.into();
        self
    }

    pub fn header(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
        self.headers.push((k.into(), v.into()));
        self
    }

    pub fn build(self) -> Request { self }
}

let r = Request::new("https://example.com")
    .method("POST")
    .header("accept", "application/json")
    .build();

ALWAYS prefer mut self -> Self builders over &mut self -> &mut Self builders when each step is logically a fresh value, because the consuming form chains cleanly into a final build() that returns an owned Self.

Pattern B: From / Into for conversions

Implement From<X> for Y and you get Into<Y> for X for free via the blanket impl:

pub struct Celsius(pub f64);
pub struct Fahrenheit(pub f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 9.0 / 5.0 + 32.0) }
}

let f: Fahrenheit = Celsius(100.0).into();   // uses the From impl
let f2 = Fahrenheit::from(Celsius(0.0));     // equivalent

ALWAYS implement From rather than Into directly; Into is then automatic. NEVER implement both From<X> for Y and Into<Y> for X by hand; they will conflict with the blanket impl.

For conversions that can fail, use TryFrom / TryInto.

Pattern C: By-value method receiver (fn into_inner(self))

A method that consumes the receiver returns owned inner data:

pub struct Wrapper<T> { inner: T }

impl<T> Wrapper<T> {
    pub fn into_inner(self) -> T { self.inner }
}

let w = Wrapper { inner: String::from("hi") };
let s: String = w.into_inner();   // w is consumed
// w used here would be E0382

ALWAYS name an into_* method when it takes self by value and returns ownership. NEVER call such a method as_* (that name is reserved for cheap borrow conversions) and NEVER call it to_* (reserved for explicit owned conversions that do not consume self).

Source: Rust API Guidelines - as/to/into


Partial moves out of fields

Moving a non-Copy field out of a struct partially invalidates the struct:

struct Pair { a: String, b: String }
let p = Pair { a: "A".into(), b: "B".into() };

let a = p.a;             // p.a moved out; p is now partly invalid
// let whole = p;        // E0382: use of partially moved value `p`
println!("{}", p.b);     // ok: p.b is still owned by p
drop(a);                 // ok

After a partial move:

  • The struct as a whole cannot be used (no let q = p;, no &p, no passing by value).
  • Other fields are still usable individually by name (p.b).
  • The remaining fields will still be dropped when p goes out of scope.

Fixing partial-move blockers

When you need the field back and still want the whole struct usable, use mem::take or mem::replace:

use std::mem;

struct Buffer { data: String }
let mut buf = Buffer { data: "payload".into() };

// Take the data out, leaving String::default() ("") behind.
let owned: String = mem::take(&mut buf.data);
// buf is still fully usable; buf.data is now "".

// Or swap a replacement in:
let prev: String = mem::replace(&mut buf.data, String::from("next"));
// buf.data is now "next"; `prev` holds "payload".
  • mem::take(&mut x) returns the value at x and leaves T::default() in its place. Requires T: Default.
  • mem::replace(&mut x, new) returns the old value at x and stores new there. Does NOT require T: Default.

ALWAYS use mem::take / mem::replace to extract owned data from behind a &mut reference. NEVER attempt to move out of &self or &mut self directly: the compiler rejects it (E0507, E0509).

Source: std::mem::take Source: std::mem::replace


Common error codes

Code Trigger Fix
E0382 "use of moved value" : a binding was moved, then used Clone before the move, restructure to borrow, or do not use afterwards
E0507 "cannot move out of borrowed content" : moving through & or &mut Use mem::take / mem::replace, clone, or redesign to take by value
E0509 "cannot move out of type ... which implements Drop" Same as E0507; Drop types cannot be partially moved out of
E0040 "explicit use of destructor method" : called .drop() directly Use the free function drop(value)
E0184 "the trait Copy may not be implemented for this type ... has a destructor" Remove Copy or remove Drop; they are mutually exclusive
E0204 "the trait Copy cannot be implemented for this type" : a field is not Copy Remove #[derive(Copy)]; keep Clone
E0515 "cannot return reference to local variable" Return an owned value instead of a reference

Full E-code drilldown lives in [[rust-errors-borrow-checker]].


Section: Avoid these mistakes

(Full list with WHYs in references/anti-patterns.md.)

  • Sprinkling .clone() everywhere to silence the borrow checker (Clippy redundant_clone).
  • Deriving #[derive(Copy, Clone)] on a struct containing String, Vec, Box, Rc, or Arc (E0204).
  • Implementing Drop on a type derived as Copy (E0184).
  • Calling value.drop() instead of drop(value) (E0040).
  • Trying to move a field out of &self or &mut self (E0507) without mem::take / mem::replace.
  • Returning a reference to a function-local value (E0515: "cannot return reference to local variable").
  • Re-using a binding after it was moved into a function or another binding (E0382).
  • Treating a partial move as "the whole struct is broken" : the individual unmoved fields are still usable.

Reference links

For deeper drill-downs see:

  • references/methods.md: trait signatures (Copy, Clone, Drop, From, Into) and free-function signatures for mem.
  • references/examples.md: complete working examples for moves, partial moves, builders, conversions, by-value receivers.
  • references/anti-patterns.md: the most common ownership mistakes with root-cause explanations and fixes.