Today we review basic Rust concepts from last class and talk about how the language manages object ownership.
Summary
- Reminder: the goals of Rust regarding memory management
- Ownership, moving, borrowing, and mutable references (from the notes on last class)
- The
Copytrait for “plain old data” (POD) - Array and string slices (immutable borrows)
- Putting it all together
Ownership and Stack Data
In the previous lesson, we saw how Rust’s ownership system manages heap-allocated data like String to prevent memory errors. Now, we’ll look at how these rules apply to simpler, stack-allocated data and collections like arrays.
This focus on memory management is central to Rust’s design. Languages like Java and Python simplify memory management by allocating most data on the heap and using a garbage collector to clean up. This is safe but can be slow and lead to performance issues like data fragmentation. C and C++ give the programmer direct control over memory, which is fast but notoriously error-prone, leading to bugs like dangling pointers and double-frees.
Rust’s goal is to provide the performance of C/C++ while guaranteeing memory safety at compile time. The ownership and borrowing rules are the mechanism for achieving this. They allow Rust to manage memory predictably without a garbage collector, ensuring that most memory management is handled efficiently on the stack.
Simple Data and the Copy Trait
The strict ownership transfer rules (moving) are essential for data stored on the heap. But for simple data types that live entirely on the stack (like integers), a full ownership move is unnecessary overhead.
For these types, Rust uses a simpler strategy: it makes a full bit-for-bit copy. This behavior is enabled by the Copy trait. A type that has the Copy trait is duplicated on assignment or when passed to a function, rather than being moved.
The term “Plain Old Data” (POD), which really comes from the C++ world,
is a good way to think about Copy types in Rust: they can be fully
and faithfully represented by their bytes, with no extra logic for
things like constructors or deconstructors.
fn main() {
// An i32 is a simple type that implements the `Copy` trait.
let security_code = 1234;
// No move occurs here. `new_code` gets a full copy of `security_code`.
let new_code = security_code;
// Both variables are still valid and can be used independently.
println!("Original code: {}", security_code);
println!("New code: {}", new_code);
}
Types that implement Copy are simple values where a cheap, bitwise copy is a complete representation of the value. This includes:
- All integer types (
u32,i32, etc.). - All floating-point types (
f32,f64). - The boolean type (
bool). - The character type (
char). - Tuples, if they only contain types that also implement
Copy. - Arrays, if they only contain types that also implement
Copy.
A type that requires any heap allocation or has a destructor, like String, can never implement Copy.
Handling Collections: Arrays and Slices
The ownership rules apply to collections like arrays as a whole.
- An array of
Copytypes (like[i32; 4]) is itself aCopytype. The entire array is copied on the stack when passed to a function. - An array of non-
Copytypes (like[String; 2]) is moved, transferring ownership of the array and its contents.
Passing entire arrays can be inefficient or restrictive. The idiomatic way to grant functions read-only access to an array’s data is to pass a slice. A slice is a reference to a contiguous sequence of elements in a collection, written as &[T]. String literals ("hello") are also a type of slice (&str). Slices borrow data without taking ownership.
Putting It All Together: A Complete Example
Here is a single program that demonstrates the different ways data can be passed to functions in Rust.
// 1. Pass by Copy: Takes a u32, which is a `Copy` type.
// The value is copied, and the original remains valid.
fn process_id(id: u32) {
println!("Processing ID (copy): {}", id);
}
// 2. Pass by Move: Takes ownership of a `String`.
// The original `String` in the calling function becomes invalid.
fn archive_report(report: String) {
println!("Archiving report (moved): {}", report);
} // `report` is dropped here.
// 3. Pass by Immutable Reference (Borrowing): Takes a slice.
// This borrows the data without taking ownership.
fn read_scores(scores: &[u32]) {
println!("Reading scores (borrowed slice): {:?}", scores);
}
// 4. Pass by Mutable Reference: Borrows a `String` and can change it.
fn update_log(log_entry: &mut String) {
log_entry.push_str(" [UPDATED]");
println!("Updated log entry (mutably borrowed): {}", log_entry);
}
fn main() {
// --- Copy ---
let user_id = 101;
process_id(user_id); // `user_id` is copied.
println!("`user_id` is still valid: {}", user_id);
println!("---");
// --- Move ---
let report_content = String::from("System status: OK");
archive_report(report_content); // `report_content` is moved.
// The next line would NOT compile:
// println!("`report_content` is no longer valid: {}", report_content);
println!("---");
// --- Borrowing (Slice) ---
let scores = [98, 85, 92];
read_scores(&scores); // `scores` is borrowed as a slice.
println!("`scores` is still valid: {:?}", scores);
println!("---");
// --- Mutable Borrowing ---
let mut log = String::from("Initial log entry");
update_log(&mut log); // `log` is mutably borrowed.
println!("`log` was changed: {}", log);
}