Introduction
Rust’s ownership system is what makes it possible to guarantee memory safety at compile time. Unlike garbage collection (Python, Java, Go) or manual management (C++), Rust enforces rules that prevent entire categories of bugs.
This guide is aimed at C++ developers transitioning to Rust, explaining ownership, borrowing, and lifetimes.
The Ownership System
Three Ownership Rules
1. Each value in Rust has an owner
2. There can only be one owner at a time
3. When the owner goes out of scope, the value is dropped
Memory Layout Example
// Stack allocation
let x = 5; // x owns the i32 value 5
// Heap allocation
let s = String::from("hello");
// s owns a String struct on the heap:
// String { ptr: 0x1000, len: 5, capacity: 5 }
// โโโ> heap: [h, e, l, l, o]
// When x goes out of scope: 5 is dropped
// When s goes out of scope: heap memory freed + String struct dropped
Ownership Transfer (Move)
let s1 = String::from("hello");
let s2 = s1; // ownership MOVES from s1 to s2
// println!("{}", s1); // ERROR: s1 no longer owns the value
println!("{}", s2); // OK: s2 owns it now
// Memory perspective:
// Before: s1 -> [h,e,l,l,o]
// After: s2 -> [h,e,l,l,o] (s1 pointer invalid)
Copy vs Move
// Types that implement Copy: i32, f64, bool, char
let x = 5;
let y = x; // x is COPIED (both own independent copies)
println!("{}, {}", x, y); // OK: both are still valid
// String does NOT implement Copy
let s1 = String::from("hello");
let s2 = s1; // MOVES (not copies)
// println!("{}", s1); // ERROR: s1 no longer valid
Borrowing (References)
Immutable References
let s1 = String::from("hello");
let s2 = &s1; // borrow s1 immutably
let s3 = &s1; // multiple immutable borrows OK
println!("{} {} {}", s1, s2, s3); // All valid
// s1, s2, s3 go out of scope (no cleanup - no ownership)
Mutable References
let mut s = String::from("hello");
let r1 = &mut s; // mutable borrow
r1.push_str(" world");
// println!("{}", s); // ERROR: s is borrowed mutably
println!("{}", r1); // OK: mutable borrow active
// After r1 goes out of scope:
let r2 = &s; // OK: mutable borrow ended, now immutable
println!("{}", r2);
Borrowing Rules
1. At any given time, you can have either:
- ONE mutable reference, OR
- MANY immutable references
2. References must always be valid
Example:
let mut v = vec![1, 2, 3];
let r1 = &v; // immutable borrow
let r2 = &v; // another immutable borrow (OK)
// let r3 = &mut v; // ERROR: can't borrow mutably while
// immutable borrows exist
println!("{} {}", r1, r2); // r1, r2 end here
let r4 = &mut v; // OK: now we can borrow mutably
r4.push(4);
Lifetimes
What Are Lifetimes?
Lifetimes ensure that references don’t outlive the data they point to.
// This won't compile
fn dangling_reference() -> &String {
let s = String::from("hello");
&s // ERROR: s doesn't live long enough
}
// Lifetime issue:
// &s points to memory that will be freed
// when s goes out of scope
Lifetime Annotations
// Explicit lifetimes needed for borrowed data in function signatures
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
let s1 = String::from("hello");
let s2 = "world";
let result = longest(&s1, s2); // result lifetime tied to s1 and s2
println!("{}", result);
// This WOULD be an error:
fn bad_lifetime<'a>(x: &'a str) -> &'a str {
let s = String::from("hello");
&s // ERROR: lifetime mismatch
}
Common Lifetime Patterns
// Single borrowed parameter - lifetime elision applies
fn first_char(s: &str) -> &str {
&s[..1]
}
// Equivalent to:
// fn first_char<'a>(s: &'a str) -> &'a str
// Multiple parameters
fn longer<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// Return owned data (no lifetime needed)
fn make_string() -> String {
String::from("hello")
}
// Struct with references
struct Book<'a> {
title: &'a str,
pages: u32,
}
let title = String::from("Rust Programming");
let book = Book {
title: &title,
pages: 300,
};
Comparing to C++
Memory Safety in C++
// C++ - potential memory leaks and use-after-free
std::string* create_string() {
return new std::string("hello"); // Leak!
}
std::string& dangerous_reference() {
std::string s("temporary");
return s; // Dangling reference!
}
int main() {
auto s = create_string();
// Need to manually delete s
delete s; // Easy to forget
auto& r = dangerous_reference(); // UB!
std::cout << r << std::endl; // Undefined behavior
}
Same Code in Rust (Safe)
fn create_string() -> String {
String::from("hello") // Owned - safe transfer
}
fn safe_reference() -> &'static str {
"temporary" // Static lifetime string
}
fn main() {
let s = create_string();
// Auto cleanup when s goes out of scope
let r = safe_reference(); // Compiler ensures validity
println!("{}", r); // Safe!
}
Move Semantics in Practice
Function Arguments
fn print_string(s: String) {
println!("{}", s);
// s is dropped here
}
fn print_string_ref(s: &String) {
println!("{}", s);
// nothing is dropped
}
let s = String::from("hello");
print_string(s); // s MOVES into function, then dropped
// println!("{}", s); // ERROR: s was moved
let s = String::from("hello");
print_string_ref(&s); // s is BORROWED
println!("{}", s); // OK: s still valid
Returning Values
// Move semantics also apply to return values
fn move_example() -> String {
let s = String::from("hello");
s // Ownership moves to caller, no copy
}
let s = move_example();
println!("{}", s); // s owns the string
// Borrowing in return
fn borrow_example(s: &String) -> &String {
s // Return the same reference
}
let s = String::from("hello");
let r = borrow_example(&s);
println!("{}", r);
Interior Mutability
When Ownership Rules Are Too Strict
// RefCell allows mutation without &mut
use std::cell::RefCell;
let x = RefCell::new(5);
*x.borrow_mut() = 10; // Mutate through shared ref
println!("{}", *x.borrow());
// Runtime panics if you try:
// let r1 = x.borrow_mut();
// let r2 = x.borrow_mut(); // PANIC: already borrowed mutably
Rc for Shared Ownership
use std::rc::Rc;
let s = Rc::new(String::from("hello"));
let s2 = Rc::clone(&s); // Shared ownership
let s3 = Rc::clone(&s); // s, s2, s3 all own it
println!("refcount: {}", Rc::strong_count(&s)); // 3
// When all go out of scope, string is dropped
Performance Implications
Zero-Cost Abstractions
Rust vs C++ memory patterns:
Operation Rust C++
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Ownership transfer 0 cycles 0 cycles
Move semantics 0 cycles copy or move
Stack allocation ~1 cycle ~1 cycle
Reference (borrow) 0 cycles 0 cycles
Lifetime checking 0 cycles 0 cycles (compiler only)
Bounds checking (default) 1-2 cycles Optional
Optimization Example
// Rust compiler optimizes move semantics
fn process_data(v: Vec<i32>) -> Vec<i32> {
let mut v = v; // Moves, no copy
v.push(42);
v
}
// Compiled to equivalent C++:
// void process_data(vector<int>& v) {
// v.push_back(42);
// }
// Only one allocation, no copies
Common Patterns
Builder Pattern
struct ServerConfig {
host: String,
port: u16,
timeout: u64,
}
impl ServerConfig {
fn new() -> Self {
ServerConfig {
host: "localhost".to_string(),
port: 8080,
timeout: 30,
}
}
fn host(mut self, host: String) -> Self {
self.host = host;
self
}
fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
}
let config = ServerConfig::new()
.host("0.0.0.0".to_string())
.port(3000);
Into Trait for Flexibility
fn accept<S: Into<String>>(input: S) {
let s = input.into(); // Convert to String
println!("{}", s);
}
accept("hello"); // &str
accept("hello".to_string()); // String
accept("hello".to_owned()); // String
Glossary
- Ownership: Each value has exactly one owner responsible for cleanup
- Move: Transfer of ownership (no copy)
- Borrow: Temporary lending of a reference
- Reference: Pointer to owned data without ownership
- Lifetime: Duration that a reference is valid
- Mutable Reference: Reference that allows modification
- Interior Mutability: Mutation without mutable reference (RefCell, Cell)
Comments