Borrowing in Depth: How Rust and C++ Disagree About Sharing Data (And Why Rust Is Right)
In C++, the relationship between a function and its data is often an ambiguous handshake; in Rust, it’s a legally binding contract. This article explores the fundamental divide between C++'s implicit references and Rust’s explicit borrowing system. Discover why Rust’s 'honesty' regarding data access—enforced strictly by the compiler—doesn't just prevent crashes, but fundamentally changes how we design and trust large-scale software architecture.
Borrowing in Depth: How Rust and C++ Disagree About Sharing Data (And Why Rust Is Right About Everything That Matters)
There's a philosophical difference between Rust and C++ that goes deeper than syntax, deeper than memory models, deeper than any individual language feature. It's about honesty.
In C++, when you pass an object to a function, the function's relationship with that data is... ambiguous. Does it take a reference? A const reference? A pointer? A const pointer? A pointer to const? A rvalue reference? The language provides all of these options, and each has subtly different semantics that the compiler may or may not enforce consistently. You can cast away const. You can alias mutable pointers. You can pass a dangling reference to a function that stores it. The compiler says "sure, whatever you want" and wishes you luck.
Rust is honest about data sharing. There are exactly two kinds of references: &T (read-only, shareable) and &mut T (read-write, exclusive). The compiler enforces these strictly. You can't cast away immutability. You can't have a mutable reference while immutable references exist. You can't create a dangling reference. Period. No escape hatches in safe code, no "well, technically the spec says..." loopholes.
This honesty has consequences that go far beyond preventing null pointer dereferences. It prevents data races. It prevents iterator invalidation. It prevents aliasing bugs. It prevents use-after-free through references. And it makes code reviews dramatically easier, because when you see &T, you know the function can't modify the data, and when you see &mut T, you know it can—and the compiler guarantees your knowledge is correct.
At RantAI, this explicitness is the foundation of our multi-module architecture. Functions declare their relationship with data in their signatures, and the compiler verifies those declarations across every call site. This article, drawn from Chapter 5, Section 5.1.3 of our guide "The Rust Programming Language," compares Rust's borrowing system with C++'s approach and explains why Rust's explicit model produces better software.
Rust's Explicit Borrowing vs. C++'s Implicit References
In C++: Ambiguity Is the Norm
// C++: What does this function do with the data?
void process(std::vector<int>& data); // Might modify it
void process(const std::vector<int>& data); // Probably doesn't modify it... unless const_cast
void process(std::vector<int>* data); // Might be null, might modify, might store
Each signature tells you something different, but none of them are guarantees. const& can be cast away. Pointers can be null. References can be stored and outlive the data. The programmer's intent is expressed, but the compiler doesn't rigorously enforce it.
In Rust: The Signature Is the Contract
fn read_data(data: &Vec<i32>) {
// Can read data, CANNOT modify it. Period.
// The compiler enforces this — no escape in safe code.
}
fn modify_data(data: &mut Vec<i32>) {
// Can read AND modify data. Has EXCLUSIVE access.
// No other reference to this data can exist simultaneously.
data.push(42);
}
fn consume_data(data: Vec<i32>) {
// OWNS the data. Will be freed when this function returns.
// Caller can no longer use it.
}
Three options. Three clear meanings. Three compiler-enforced guarantees. No ambiguity. No "it depends on whether someone used const_cast." The function signature tells you everything about its relationship with the data, and the compiler makes sure the implementation matches.
The Dot Operator: Convenience Without Compromise
struct Point { x: f64, y: f64 }
impl Point {
fn distance(&self) -> f64 { // Takes &self — immutable borrow
(self.x * self.x + self.y * self.y).sqrt()
}
}
fn main() {
let p = Point { x: 3.0, y: 4.0 };
let d = p.distance(); // Auto-borrows: equivalent to (&p).distance()
println!("Distance: {}", d);
}
Rust auto-dereferences through the dot operator—you write p.distance() instead of (&p).distance(). This gives you the convenience of C++'s implicit references for method calls while keeping the explicitness of & and &mut for function parameters. The small things matter: method calls are ergonomic, parameter passing is explicit.
The Two Rules, Revisited with C++ Comparisons
Rule 1: Multiple &T OR one &mut T
fn main() {
let mut data = vec![1, 2, 3];
// Multiple immutable borrows — safe, like C++ const references
let a = &data;
let b = &data;
println!("{:?} {:?}", a, b);
// Mutable borrow — exclusive, unlike C++ non-const references
let c = &mut data;
c.push(4);
// Can't use a or b here — they'd see modified data
}
In C++, you can have const& and & to the same data simultaneously. The compiler allows it. If the non-const reference modifies the data while the const reference is reading it—undefined behavior. In Rust, this cannot happen. The compiler ensures exclusivity.
Rule 2: No dangling references
// This doesn't compile — and it shouldn't
fn dangling() -> &String {
let s = String::from("hello");
&s // COMPILE ERROR: s is dropped at the end of this function
}
In C++, returning a reference to a local variable compiles without complaint and crashes at runtime (or worse, appears to work with stale stack data). In Rust, the compiler prevents it entirely.
Iterator Invalidation: The Bug That Can't Happen
fn main() {
let mut data = vec![1, 2, 3, 4, 5];
// This won't compile — and that's the point
// for &item in &data {
// if item == 3 {
// data.push(6); // COMPILE ERROR: can't mutate while iterating
// }
// }
// The safe alternative:
data.retain(|&x| x != 3); // Remove 3s without iterator invalidation
}
In C++ and Java, modifying a collection while iterating over it is a classic bug—it invalidates iterators and produces undefined behavior or ConcurrentModificationException. In Rust, it's a compile error. The immutable borrow from the iterator prevents mutable access to the collection. The bug doesn't exist because the compiler prevents the code that would cause it from compiling.
Broader Implications: Trust Through the Type System
At RantAI, Rust's borrowing system has fundamentally changed how our teams collaborate. When engineer A writes a function that takes &Config, engineer B knows the function won't modify the configuration—not because of a comment, not because of a code review, but because the compiler enforces it. When engineer C writes a function that takes &mut State, engineer D knows to review it carefully for state mutation. The type system encodes intent, the compiler enforces it, and the team trusts it.
This trust compounds as codebases grow. In a million-line C++ codebase, understanding data flow requires tracing through every function call, every reference, every pointer. In a million-line Rust codebase, the function signatures tell you the story. &T means read-only. &mut T means exclusive mutation. T means ownership transfer. The signatures are the architecture documentation, and they're always correct because the compiler enforces them.
Practical Applications & Strategic Takeaways
For newcomers: Think of &T as "looking at" data and &mut T as "working on" data. Default to &T. Use &mut T only when you need to modify.
For C++ veterans: Rust's &T is what const& would be if const were truly immutable (no const_cast, no mutable members). Rust's &mut T is what & would be if the compiler enforced exclusive access.
For architects: The three-way choice—T (own), &T (read), &mut T (write)—is the most important API design decision in Rust. Choose the weakest option that satisfies the function's needs. This produces the most flexible, most composable, and most clearly documented APIs.
Our Commitment to Open Knowledge
RantAI is committed to open education. Borrowing compared to C++ is covered in Chapter 5, Section 5.1.3 of our guide, "The Rust Programming Language," freely available online.
Explore these concepts further: https://trpl.rantai.dev
Support Our Mission & Get Your Handbook
Get the Handbook on Amazon KDP: https://www.amazon.com/dp/B0DHCMD3F2
Get the Handbook on Google Play Books: https://play.google.com/store/books/details?id=INwfEQAAQBAJ
Have you been bitten by C++'s implicit reference semantics? Or do you have a war story about iterator invalidation that Rust would have prevented? Share below!
#RustLang #Borrowing #MemorySafety #CppComparison #RantAI #LearnRust #References #DataRaces #SystemsProgramming #CompileTimeSafety
Want to learn more?
Connect with our team to discuss how AI can transform your enterprise.