Fundamentals 9 min read

Four Lines of Code: C++ vs Rust Safety Showdown

A concise vector reallocation example demonstrates how C++ permits use-after-free bugs that Rust catches at compile time via borrow checking, then explores ownership, move semantics, and thread-safety differences between the two languages.

IT Services Circle
IT Services Circle
IT Services Circle
Four Lines of Code: C++ vs Rust Safety Showdown

Core Example: Vector Reallocation

The article opens with a minimal C++ snippet:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3};
    int& first = v[0];          // reference to first element
    v.push_back(4);               // may trigger reallocation
    std::cout << first << "
"; // undefined behavior if reallocation occurred
    return 0;
}

When push_back exceeds capacity, the vector reallocates, moves elements, and frees the old buffer. The reference first becomes a dangling reference; accessing it is a use-after-free. The bug may surface only after many runs, turning into a production crash.

The equivalent Rust code:

fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];
    v.push(4);
    println!("{}", first);
}

This fails to compile with error E0502: cannot borrow v as mutable while immutable borrow first is still in use. The borrow checker detects the conflict at compile time, eliminating a whole class of runtime errors.

Performance Is Not the Primary Differentiator

Both languages compile to native code without tracing GC. For well-optimized tight loops, generated code is nearly identical. Real-world performance depends on algorithms, data layout, and compiler flags—not language name alone. Claims that "X is always faster" should be treated skeptically; benchmark your own workloads.

Borrow Checking Rules

Rust's borrow checker enforces: for any data, you may have either multiple shared references or a single mutable reference, but not both simultaneously. In the example, first holds a shared borrow used later by println!, while push requires a mutable borrow of v. The conflict is caught at compile time with zero runtime overhead. Rust does retain some runtime checks (e.g., array bounds, RefCell<T>), but the borrow checker itself is purely static.

Move Semantics

C++'s std::move is a cast enabling move constructors/assignment; moved-from objects remain in a "valid but unspecified" state and can still be used (often incorrectly). Rust's move is default for non- Copy types: ownership transfers, and the source binding becomes inaccessible—any further use is a compile error:

let a = String::from("hello");
let b = a;          // ownership moves to b
println!("{}", a);   // error: borrow of moved value

This prevents double-free and use-after-move. Shared ownership is opt-in via Rc<T> or Arc<T>.

Thread Safety

In C++, unsynchronized concurrent access to an int is a data race—undefined behavior allowing compiler optimizations that break assumptions. Safe Rust forbids this: sharing mutable data across threads requires synchronization primitives like Mutex or atomics. The traits Send (safe to transfer between threads) and Sync (safe to share via reference) encode these guarantees in the type system.

Rust's Mutex wraps the data, forcing lock acquisition before access:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    for _ in 0..4 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            let mut n = counter.lock().unwrap();
            *n += 1;
        }));
    }
    for h in handles { h.join().unwrap(); }
    println!("{}", *counter.lock().unwrap());
}

The mutex guards the data; you cannot read or modify the integer without locking. In C++, the mutex and data are often separate, relying on discipline. Rust prevents data races in safe code but cannot eliminate deadlocks or logic races—careful design remains necessary.

Conclusion

Starting from a vector reallocation bug, the article contrasts C++'s reliance on programmer discipline (valid references, correct resource management, manual synchronization) with Rust's compile-time enforcement via ownership, borrowing, and type-based thread safety. These mechanisms shift dangling references, double-frees, and data races from runtime failures to compile-time errors—without removing the need for correct concurrency logic.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

RustC++thread safetymemory safetymove semanticsownershipborrow checkeruse-after-free
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.