8.3 Function Parameters and Data Passing

Rust functions can accept parameters in various forms, each affecting ownership, mutability, and borrowing. Within a function’s body, parameters behave like ordinary variables. This section describes the fundamental parameter types, when to use them, and how they compare to C function parameters.

We will illustrate parameter passing with the String type, which is moved into the function when passed by value and can no longer be used at the call site. Note that primitive types implementing the Copy trait will be copied by value instead of moved.

8.3.1 Passing by Value (T)

When a parameter has type T (and T does not implement Copy), the value is moved into the function. The function takes ownership, and the original variable in the caller’s scope becomes inaccessible.

// This function takes ownership of the String.
fn process_string(s: String) {
    println!("Processing owned string: {}", s);
    // 's' goes out of scope here, and the memory is deallocated.
}

fn main() {
    let message = String::from("Owned data");
    process_string(message); // Ownership of 'message' is transferred to process_string.
    // Trying to use 'message' here would cause a compile-time error:
    // println!("Original message: {}", message); // Error: value borrowed after move
}
  • Use Cases: Primarily when the function needs to consume the value (e.g., send it elsewhere, store it permanently) or take final ownership (ensuring the value is dropped or managed exclusively by the function). This pattern guarantees that the original variable cannot be used after the call. It’s also used when the function manages the lifecycle of a resource represented by T. While a function fn transform(value: T) -> U can exist, if value isn’t modified in place (which it can’t be if T isn’t mut), often taking &T might be more flexible if the original isn’t meant to be consumed.
  • Comparison to C: Similar to passing a struct by value, but Rust’s borrow checker prevents using the original variable after the move.

8.3.2 Passing by Mutable Value (mut T)

You can declare a value parameter as mutable using mut T. Ownership is still transferred (for non-Copy types), but the function is allowed to modify the value it now owns.

// This function takes ownership and can modify the owned value.
fn modify_string(mut s: String) { // 'mut s' allows modification inside the function
    s.push_str(" (modified)");
    println!("Modified owned string: {}", s);
    // s is dropped here unless returned
}

// Example of modifying and returning ownership
fn modify_and_return(mut s: String) -> String {
    s.push_str(" and returned");
    s // Return ownership of the modified string
}

fn main() {
    // NOTE: 'message' does NOT need to be 'mut' here!
    let message = String::from("Mutable owned data");
    // modify_string takes ownership, message cannot be used after
    modify_string(message);
    // println!("{}", message); // Error: use of moved value

    let message2 = String::from("Another message");
    // modify_and_return takes ownership, but returns it
    let modified_message2 = modify_and_return(message2);
    // println!("{}", message2); // Error: use of moved value 'message2'
    println!("{}", modified_message2); // Ok: "Another message and returned"
}

Note on Caller Variable Mutability: Notice in the examples that message and message2 were declared using let, not let mut. When passing by value (mut T), the function takes full ownership via a move. The mut in the function signature (e.g., mut s: String) only grants the function permission to mutate the value it now exclusively owns. Since the caller loses ownership and cannot access the original variable after the move, whether the original variable was declared mut is irrelevant.

This contrasts sharply with passing a mutable reference (&mut T), where the caller retains ownership and merely lends out mutable access. To grant this mutable borrow permission, the caller’s variable must be declared with let mut.

  • Use Cases: When the function needs to take ownership and modify the value it now owns. This could be for internal computations, using the value as a mutable scratch space, or for patterns like functional builders/chaining. In such patterns, a configuration object or state might be passed through several functions, each taking ownership via mut T, modifying it in place, and then returning ownership (fn step(mut config: Config) -> Config). This can be efficient as it may avoid allocations needed if new instances were created at each step. However, for simply modifying the caller’s original data without transferring ownership back and forth, &mut T remains the more common choice.
  • Comparison to C: Similar to passing a struct by value regarding locality (changes don’t affect the caller), but distinct due to Rust’s move semantics. Modifications inside the function apply only to the specific instance whose ownership was transferred into the function via the move.

8.3.3 Passing by Shared Reference (&T)

To allow a function to read data without taking ownership, pass a shared reference (&T). This is known as borrowing. The caller retains ownership, and the data must remain valid while the reference exists.

// This function borrows the String immutably.
fn calculate_length(s: &String) -> usize {
    s.len() // Can read from 's', but cannot modify it.
}

fn main() {
    let message = String::from("Immutable borrow");
    let length = calculate_length(&message); // Pass a reference to 'message'.
    println!("The length of '{}' is {}", message, length);
    // 'message' is still valid and owned here.
}
  • Use Cases: Very common when a function only needs read-access to data. Avoids costly cloning or ownership transfer.
  • Comparison to C: Similar to passing a pointer to const data (e.g., const char* or const MyStruct*). Rust guarantees at compile time that the referenced data cannot be mutated through this reference and that the data outlives the reference.

8.3.4 Passing by Mutable Reference (&mut T)

To allow a function to modify data owned by the caller, pass a mutable reference (&mut T). This is also borrowing, but exclusively – while the mutable reference exists, no other references (mutable or shared) to the data are allowed.

// This function borrows the String mutably.
fn append_greeting(s: &mut String) {
    s.push_str(", World!"); // Can modify the borrowed String.
}

fn main() {
    // 'message' must be declared 'mut' to allow mutable borrowing.
    let mut message = String::from("Hello");
    append_greeting(&mut message); // Pass a mutable reference.
    println!("Modified message: {}", message); // Output: Modified message: Hello, World!
    // 'message' is still owned here, but its content has been changed.
}
  • Use Cases: Very common when a function needs to modify data in place without taking ownership (e.g., modifying elements in a vector, updating fields in a struct).
  • Comparison to C: Similar to passing a non-const pointer (e.g., char* or MyStruct*) to allow modification. Rust’s borrow checker provides stronger safety guarantees by preventing simultaneous mutable access or mixing mutable and shared access, eliminating data races at compile time.

8.3.5 Summary Table: Choosing Parameter Types

Parameter TypeOwnershipModification of OriginalCaller Variable mut Required?Typical Use CaseC Analogy (Approximate)
T (non-Copy)TransferredNoNoConsuming data, final ownership transferPass struct by value
T (Copy type)CopiedNoNoPassing small, cheap-to-copy dataPass primitive by value
mut T (non-Copy)TransferredNo (Local owned value)NoModifying owned value before consumption/returnPass struct by value
&TBorrowedNoNoRead-only access, avoiding copiesconst T*
&mut TBorrowedYesYesModifying caller’s data in-placeT* (non-const)

(Self-correction: Minor tweak in table description for mut T to be clearer)

Note on Shadowing Parameters: You can declare a new local variable with the same name as an immutable parameter, making it mutable within the function’s scope. This is called shadowing.

fn process_value(value: i32) {
    // 'value' parameter is immutable.
    // Shadow 'value' with a new mutable variable.
    let mut value = value;
    value += 10;
    println!("Processed value: {}", value);
}

fn main() {
    process_value(5); // Prints: Processed value: 15
}

Side Note on mut with Reference Parameters: In Rust, you might occasionally encounter function signatures like fn func(mut param: &T) or fn func(mut param: &mut T). Adding mut directly before the parameter name (mut param) makes the binding param mutable within the function’s scope. This means you could reassign param to point to a different value of type &T or &mut T respectively.

  • For mut param: &T, this does not allow modifying the data originally pointed to by param, because the type &T represents a shared, immutable borrow.
  • For mut param: &mut T, the underlying data can be modified because the type &mut T is a mutable borrow, regardless of whether the binding param itself is mut.

This pattern of making the reference binding itself mutable is relatively uncommon in idiomatic Rust compared to simply passing &T or &mut T.