Academy
Ownership in Functions: Who Owns What After the Call
Discover how Rust function signatures act as absolute contracts—revealing whether data is consumed, read, or modified at compile time.
6 min read
On this page
- Taking Ownership: The Function Consumes the Data
- Immutable Borrowing: Reading Without Consuming
- Mutable Borrowing: Exclusive Modification
- Returning Ownership: Giving Data Back
- The Decision Framework
- Broader Implications: Signatures as Architecture
- Practical Applications & Strategic Takeaways
- Our Commitment to Open Knowledge
- Support Our Mission & Get Your Handbook
Here's a question that every programmer answers dozens of times a day, usually without thinking about it: when I pass data to a function, what happens to it? Can I still use it after the call? Did the function modify it? Did the function store it somewhere? Will it be freed when the function returns?
In most languages, the answer is "it depends"—on whether you passed a pointer or a value, on whether the language uses reference semantics or value semantics, on whether the function's documentation (if it exists) accurately describes its behavior (if the documentation is up to date). In C++, passing a std::unique_ptr transfers ownership, but passing a reference doesn't—unless the function calls std::move on it, which it technically shouldn't do with a reference but nothing prevents it.
In Rust, the function signature tells you everything. fn process(data: String) takes ownership—the caller loses the data. fn analyze(data: &String) borrows immutably—the caller keeps the data, and the function can only read it. fn modify(data: &mut String) borrows mutably—the caller keeps the data, but the function has exclusive write access. No ambiguity. No "check the documentation." No "hope the implementation matches the intent." The signature is the contract, and the compiler enforces it.
At RantAI, function signatures are our primary API documentation. When an engineer reads fn ingest(records: Vec<Record>), they know the function consumes the records. When they read fn validate(records: &[Record]) -> Vec<Error>, they know the function reads without consuming. The signatures tell the ownership story. This article, drawn from Chapter 9, Section 9.2 of our guide "The Rust Programming Language," covers how ownership and borrowing work in function parameters and return values.
Taking Ownership: The Function Consumes the Data#
fn process_and_archive(data: Vec<Record>) {
for record in &data {
validate(record);
}
archive_to_database(data); // data is moved again—into the archive function
} // If data wasn't moved, it would be dropped here
fn main() {
let records = load_records();
process_and_archive(records); // Ownership moves into the function
// records is INVALID here — it was consumed by the function
}
When a function takes a parameter by value (data: Vec<Record>), it takes ownership. The caller's variable is invalidated. The function is responsible for the data's lifecycle—it can use it, transform it, pass it to another function, or let it be dropped when it goes out of scope.
When to take ownership: When the function needs to store the data permanently (in a struct field, in a collection), when it needs to transfer the data elsewhere (to another thread, to a database), or when the caller explicitly doesn't need the data anymore.
Immutable Borrowing: Reading Without Consuming#
fn count_valid(records: &[Record]) -> usize {
records.iter().filter(|r| r.is_valid()).count()
}
fn summarize(records: &[Record]) -> Summary {
Summary {
total: records.len(),
valid: count_valid(records), // Can pass the borrow to other functions
}
}
fn main() {
let records = load_records();
let summary = summarize(&records); // Borrow — records stays valid
println!("Summary: {:?}", summary);
println!("Records still available: {}", records.len()); // Still valid!
}
Immutable borrowing (&T or &[T]) lets the function read the data without taking ownership. The caller retains ownership and can continue using the data after the call. Multiple functions can borrow the same data simultaneously.
When to borrow immutably: This should be your default for function parameters. If the function only needs to read the data, take a reference. This is the most flexible option—the caller keeps the data and can pass it to other functions too.
Mutable Borrowing: Exclusive Modification#
fn normalize(records: &mut Vec<Record>) {
for record in records.iter_mut() {
record.name = record.name.trim().to_lowercase();
record.email = record.email.trim().to_lowercase();
}
}
fn deduplicate(records: &mut Vec<Record>) {
records.sort_by(|a, b| a.email.cmp(&b.email));
records.dedup_by(|a, b| a.email == b.email);
}
fn main() {
let mut records = load_records();
normalize(&mut records); // Mutably borrows, modifies in place
deduplicate(&mut records); // Mutably borrows again (previous borrow ended)
println!("Clean records: {}", records.len());
}
Mutable borrowing (&mut T) gives the function exclusive access to modify the data. While the function holds the mutable borrow, no other code can access the data. This exclusivity prevents data races and ensures the function's modifications aren't observed in an inconsistent state.
Returning Ownership: Giving Data Back#
fn create_report(records: &[Record]) -> Report {
let summary = calculate_summary(records);
let charts = generate_charts(records);
Report { summary, charts } // Ownership of Report moves to caller
}
fn transform(data: Vec<i32>) -> Vec<i32> {
data.into_iter().map(|x| x * 2).collect() // Takes ownership, returns new owned data
}
fn main() {
let records = load_records();
let report = create_report(&records); // Borrows records, returns owned Report
let numbers = vec![1, 2, 3];
let doubled = transform(numbers); // Consumes numbers, returns new vec
}
Return values transfer ownership to the caller. The caller receives owned data that it's responsible for. This is Rust's equivalent of "the function allocates, the caller frees"—except the freeing is automatic when the owner goes out of scope.
The Decision Framework#
Parameter Type Meaning Caller After Call Use When T Takes ownership Can't use the value Storing, transferring, consuming &T Immutable borrow Still has the value Reading (default choice) &mut T Mutable borrow Still has the value (modified) Modifying in place
Broader Implications: Signatures as Architecture#
At RantAI, we review function signatures as architectural decisions. A function that takes Vec<Record> by value is a pipeline terminus—data flows in and doesn't come back. A function that takes &[Record] is a pipeline observer—it reads without affecting the flow. A function that takes &mut Vec<Record> is a pipeline transformer—it modifies the data in transit. The signatures define the pipeline architecture, and the compiler verifies the data flow.
Practical Applications & Strategic Takeaways#
For newcomers: Start with &T for all function parameters. Switch to &mut T when you need modification. Switch to T when you need permanent storage. This progression—read, modify, own—covers virtually all cases.
For API designers: Your function signatures are your most important documentation. A user reading fn encrypt(plaintext: String) -> Vec<u8> knows the plaintext is consumed (can't be accidentally leaked). That's a security property encoded in the type system.
For performance engineers: Borrowing avoids copying. Passing &Vec<i32> to a function copies 8 bytes (a pointer). Passing Vec<i32> moves 24 bytes (pointer + length + capacity). Cloning Vec<i32> copies the entire contents. Choose the cheapest option that satisfies your requirements.
Our Commitment to Open Knowledge#
RantAI is committed to open education. Ownership in functions is covered in Chapter 9, Section 9.2 of our guide, "The Rust Programming Language," freely available online.
Explore these concepts further: https://trpl.rantai.dev
Support Our Mission & Get Your Handbook#
Get the Handbook on Amazon KDP: https://www.amazon.com/dp/B0DHCMD3F2
Get the Handbook on Google Play Books: https://play.google.com/store/books/details?id=INwfEQAAQBAJ
How do you decide between taking ownership and borrowing in your function signatures? Share your design principles below!
#RustLang #Ownership #Functions #Borrowing #RantAI #LearnRust #APIDesign #MemorySafety #Parameters #SystemsProgramming
Next article
Common Ownership Pitfalls: The Mistakes Every Rust Newcomer Makes