Memory Safety: Rust vs C++

Memory safety bugs represent some of the most critical vulnerabilities in software, responsible for approximately 70% of security issues in systems software. Rust and C++ take fundamentally different philosophical approaches to solving this problem.

The Philosophical Divide

C++: Trust the Programmer

C++ follows the principle of "zero-overhead abstraction" and trusts developers to manage memory correctly. The language provides tools but doesn't enforce their use.

Rust: Enforce Safety at Compile Time

Rust takes the stance that memory safety should be guaranteed by the compiler. If code compiles, it's memory safe (outside of explicit unsafe blocks).

Ownership and Borrowing

The most fundamental difference between Rust and C++ lies in how they handle ownership of data.

C++: Manual Management

In C++, ownership is a convention rather than a language feature. Modern C++ uses smart pointers, but they're optional.

C++
// Raw pointer - who owns this?
int* data = new int(42);
process(data);
// Did process() delete it? Should we?

// Smart pointer - clearer ownership
std::unique_ptr<int> data = std::make_unique<int>(42);
process(std::move(data));
// Ownership transferred, data is now null

Rust: Ownership Enforced

Rust makes ownership a core language feature with compile-time enforcement.

Rust
let data = Box::new(42);
process(data);
// Ownership moved - compiler prevents further use

// This would be a compile error:
// println!("{}", data); // ERROR: value used after move
Key Insight: In Rust, ownership rules are checked at compile time, making it impossible to accidentally use moved values. In C++, this is only a convention that can be violated.

The Use-After-Free Problem

Use-after-free bugs occur when code accesses memory that has already been deallocated. This is one of the most common and dangerous memory safety issues.

C++: Runtime Danger

C++
std::vector<int> vec = {1, 2, 3, 4, 5};
int* ptr = &vec[0];  // Pointer to first element

vec.push_back(6);    // May reallocate!

// ptr might now point to deallocated memory
std::cout << *ptr;   // Undefined behavior!
Warning: This C++ code compiles without errors but can crash or exhibit unpredictable behavior at runtime.

Rust: Compile-Time Prevention

Rust
let mut vec = vec![1, 2, 3, 4, 5];
let ptr = &vec[0];    // Immutable borrow

vec.push(6);          // COMPILE ERROR!
// Cannot borrow vec as mutable because it's borrowed as immutable

println!("{}", ptr);
Key Insight: Rust's borrow checker prevents this at compile time by enforcing that you cannot modify data while references to it exist.

Data Races and Concurrency

Data races occur when multiple threads access the same memory without proper synchronization. They're notoriously difficult to debug.

C++: Manual Synchronization

C++
class Counter {
    int count = 0;
public:
    void increment() { 
        count++; // Not thread-safe!
    }
    int get() { return count; }
};

// Nothing prevents misuse:
Counter c;
std::thread t1([&]() { c.increment(); });
std::thread t2([&]() { c.increment(); }); // Data race!

Rust: Fearless Concurrency

Rust
use std::sync::Mutex;
use std::thread;

let counter = Mutex::new(0);

// This won't compile - Mutex not shareable between threads
// let handle = thread::spawn(|| {
//     *counter.lock().unwrap() += 1; // ERROR!
// });

// Must use Arc (Atomic Reference Counting) for sharing
use std::sync::Arc;
let counter = Arc::new(Mutex::new(0));
let counter_clone = Arc::clone(&counter);

let handle = thread::spawn(move || {
    *counter_clone.lock().unwrap() += 1; // Safe!
});
Key Insight: Rust's type system makes data races impossible. You cannot share mutable data between threads unless it's wrapped in proper synchronization primitives.

Null Pointer Dereferencing

C++: The Billion Dollar Mistake

C++
int* find_value(std::vector<int>& vec, int target) {
    for (auto& val : vec) {
        if (val == target) return &val;
    }
    return nullptr; // Indicates not found
}

// Caller must remember to check
int* result = find_value(vec, 42);
std::cout << *result; // Crash if nullptr!

Rust: Option Types

Rust
fn find_value(vec: &Vec<i32>, target: i32) -> Option<&i32> {
    for val in vec {
        if *val == target {
            return Some(val);
        }
    }
    None
}

// Must handle the None case explicitly
match find_value(&vec, 42) {
    Some(val) => println!("{}", val),
    None => println!("Not found"),
}
Key Insight: Rust eliminates null pointer dereferences by not having null pointers. Instead, it uses the Option type, forcing developers to explicitly handle the "not found" case.

The Cost of Safety

C++: Performance First

C++ prioritizes zero-cost abstractions. If you don't use a feature, you don't pay for it. Memory safety features like bounds checking are opt-in and often disabled in release builds.

Rust: Safety Without Compromise

Rust achieves memory safety with zero runtime overhead. The borrow checker operates at compile time, adding no runtime cost. In practice, Rust and C++ have comparable performance.

Rust
// No runtime overhead - bounds checked at compile time
let arr = [1, 2, 3];
let x = arr[0]; // Safe

// Compile-time error prevents out-of-bounds access
// let y = arr[5]; // ERROR: index out of bounds

When to Use unsafe

Rust acknowledges that sometimes you need to bypass the borrow checker for performance or to interface with hardware/C libraries.

Rust
// Raw pointer access requires unsafe block
unsafe {
    let ptr = 0x1234 as *const i32;
    // Now we're in C++ territory - be careful!
    println!("{}", *ptr);
}

// unsafe is explicit and localized
// Most Rust code never needs it

Conclusion

C++ and Rust represent two different philosophies for systems programming:

Neither approach is universally superior. C++ offers mature tooling, vast libraries, and gradual adoption of safety features. Rust provides stronger guarantees but requires learning a new paradigm and can involve more upfront development time fighting the borrow checker.

The choice depends on your priorities: Do you value proven ecosystems and developer freedom, or are you willing to invest in learning a stricter model that eliminates memory safety bugs at compile time?