Skip to main content
โšก Calmops

Rust Memory Safety: Ownership Deep Dive for C++ Developers

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)

Resources


Comments