Skip to content

Q037. How does ownership cross function parameters, return values, and method receivers?

Short Answer

A parameter of type T receives a value by ownership transfer for non-Copy types or by copying for Copy types. &T and &mut T borrow instead. Returning T transfers ownership to the caller. Method receivers express the same choices: self consumes the receiver, &self observes it, and &mut self mutates it in place.

Deep Dive

Function signatures are ownership contracts:

  • fn consume(value: T) allows retention, destruction, or transfer of value;
  • fn inspect(value: &T) temporarily reads without taking ownership;
  • fn update(value: &mut T) temporarily mutates while the caller retains ownership;
  • fn create() -> T gives the caller an owned result.

Method receiver syntax is shorthand for a first parameter. Consuming self is useful for state transitions, conversions, builders, and methods that return an owned field. Borrowed receivers support repeated use of the same object.

Returning an owned heap-backed value does not imply deep-copying its allocation. Compiler and ABI choices determine physical transfer, while the language guarantees that the caller receives ownership.

Internal Model

At call boundaries, MIR records Move or Copy operands for by-value arguments and reference values for borrows. The ABI may pass small values in registers, larger values indirectly, or construct return values directly in caller-provided storage.

Ownership semantics remain independent of those optimizations: parameters and return places have defined drop responsibilities even when no physical copy occurs.

Example

rust
struct Request {
    path: String,
}

impl Request {
    fn path(&self) -> &str {
        &self.path
    }

    fn replace_path(&mut self, path: String) {
        self.path = path;
    }

    fn into_path(self) -> String {
        self.path
    }
}

fn normalize(mut text: String) -> String {
    text.make_ascii_lowercase();
    text
}

fn main() {
    let path = normalize(String::from("/ORDERS"));
    let mut request = Request { path };

    println!("{}", request.path());
    request.replace_path(String::from("/health"));

    let path = request.into_path();
    println!("{path}");
}

Common Mistakes

  • Assuming every by-value parameter physically copies heap data.
  • Taking self when a method only needs to inspect the receiver.
  • Returning references without accounting for which input owner keeps the referent alive.

Follow-ups

  • When should a builder consume and return self?
  • How can a method move one field out of self safely?
  • How does return-place optimization interact with ownership semantics?

References

  • The Rust Programming Language
  • Rust Reference
  • Rust API Guidelines