skip to content
Mehdi Mehdikhani
Table of Contents

Rust’s ownership system is probably most unique thing about the language. Coming from C++, it felt weird at first, but now I can’t imagine going back to manual memory management.

What Ownership Actually Is

Every value in Rust has exactly one owner at any time. When the owner goes out of scope, the value is automatically dropped:

fn main() {
let s = String::from("hello"); // s owns the string
println!("{}", s);
} // s goes out of scope, string is automatically freed

No delete calls, no memory leaks, no double-free bugs. The compiler tracks everything.

The Move Semantics

Unlike C++ where you copy by default, Rust moves by default for non-Copy types:

let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2
// println!("{}", s1); // Error! s1 no longer owns the data
println!("{}", s2); // OK, s2 owns it now

This prevents the classic double-free bug where two variables try to free the same memory.

When I Actually Use Ownership

Most of the time, I work with ownership when:

  1. Building data structures: Deciding who owns what:
struct User {
name: String, // User owns the name
email: String, // User owns the email
}
struct UserManager {
users: Vec<User>, // UserManager owns all users
}
impl UserManager {
fn add_user(&mut self, user: User) {
self.users.push(user); // Ownership transfers to the vector
}
fn get_user(&self, index: usize) -> Option<&User> {
self.users.get(index) // Return a borrow, not ownership
}
}
  1. Function parameters: Choosing between taking ownership or borrowing:
// Takes ownership - function consumes the string
fn process_and_consume(data: String) {
println!("Processing: {}", data);
// data is dropped when function ends
}
// Borrows immutably - can read but not modify
fn process_readonly(data: &String) {
println!("Reading: {}", data);
// Original owner still has the data
}
// Borrows mutably - can modify
fn process_and_modify(data: &mut String) {
data.push_str(" - processed");
println!("Modified: {}", data);
}
  1. Resource management: RAII patterns work beautifully:
use std::fs::File;
use std::io::prelude::*;
fn write_log(message: &str) -> std::io::Result<()> {
let mut file = File::create("app.log")?; // File owns the handle
file.write_all(message.as_bytes())?;
Ok(())
// File is automatically closed when it goes out of scope
}
  1. Avoiding unnecessary clones: Understanding when to move vs borrow:
struct Config {
database_url: String,
port: u16,
}
impl Config {
// Takes ownership of the string - no unnecessary clone
fn new(database_url: String, port: u16) -> Self {
Self { database_url, port }
}
// Returns a borrow for reading
fn get_database_url(&self) -> &str {
&self.database_url
}
}
// Usage
let config = Config::new("postgres://localhost".to_string(), 8080);
println!("Connecting to: {}", config.get_database_url());

Borrowing Rules

Rust has strict borrowing rules that prevent data races:

  1. You can have either one mutable reference OR multiple immutable references
  2. References must always be valid
let mut data = vec![1, 2, 3];
let r1 = &data; // Immutable borrow
let r2 = &data; // Another immutable borrow - OK
println!("{:?} {:?}", r1, r2);
let r3 = &mut data; // Mutable borrow
// println!("{:?}", r1); // Error! Can't use immutable borrow while mutable exists
r3.push(4); // OK, using the mutable borrow
println!("{:?}", r3);

Real World Example

Here’s a simple cache implementation showing ownership patterns:

use std::collections::HashMap;
struct Cache<K, V> {
data: HashMap<K, V>,
max_size: usize,
}
impl<K, V> Cache<K, V>
where
K: std::hash::Hash + Eq + Clone,
{
fn new(max_size: usize) -> Self {
Self {
data: HashMap::new(),
max_size,
}
}
// Takes ownership of key and value
fn insert(&mut self, key: K, value: V) {
if self.data.len() >= self.max_size {
// Simple eviction: clear half the cache
let keys_to_remove: Vec<K> = self.data
.keys()
.take(self.max_size / 2)
.cloned()
.collect();
for key in keys_to_remove {
self.data.remove(&key);
}
}
self.data.insert(key, value);
}
// Borrows key to look up, returns optional borrow of value
fn get(&self, key: &K) -> Option<&V> {
self.data.get(key)
}
// Takes ownership of key, returns ownership of value if found
fn remove(&mut self, key: &K) -> Option<V> {
self.data.remove(key)
}
}
// Usage
let mut cache = Cache::new(100);
cache.insert("user:123".to_string(), "John Doe".to_string());
if let Some(name) = cache.get(&"user:123".to_string()) {
println!("Found user: {}", name); // name is borrowed
}

Working with References

References are non-owning pointers. They’re like C++ references but with compile-time safety:

fn find_longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
let string1 = String::from("hello");
let string2 = "world";
let result = find_longest(&string1, string2);
println!("Longest: {}", result);

The 'a lifetime annotation tells the compiler that the returned reference lives as long as the shorter of the two input references.

Common Patterns I Use

  1. Borrowing for function parameters when you don’t need ownership:
// Good: borrow when you only need to read
fn print_user_info(user: &User) {
println!("{}: {}", user.name, user.email);
}
// Less efficient: taking ownership unnecessarily
fn print_user_info_bad(user: User) {
println!("{}: {}", user.name, user.email);
// user is dropped here, can't be used by caller anymore
}
  1. Using &mut for in-place modifications:
fn normalize_email(email: &mut String) {
*email = email.to_lowercase();
email.retain(|c| !c.is_whitespace());
}
let mut email = String::from(" John@Example.Com ");
normalize_email(&mut email);
println!("{}", email); // "john@example.com"
  1. Clone when you need independent copies:
#[derive(Clone)]
struct Config {
settings: HashMap<String, String>,
}
fn create_dev_config(base: &Config) -> Config {
let mut dev_config = base.clone(); // Independent copy
dev_config.settings.insert("debug".to_string(), "true".to_string());
dev_config
}

The Mental Model

Think of ownership like real-world ownership:

  • Only one person can own a car at a time
  • You can lend your car (borrow) but you still own it
  • You can’t lend your car to two people for racing at the same time (no multiple mutable borrows)
  • If you sell your car (move), you don’t own it anymore

Once this clicks, Rust’s error messages start making sense. The compiler is just enforcing these logical rules at compile time, preventing the runtime crashes you’d get in other languages.

The learning curve is steep initially, but the payoff is huge: memory safety without garbage collection, and fearless concurrency because data races are impossible.