In fact, we can see this "defaults matter" problem in Rust as well. Note that Rust by-default assumes that code is running in a context where a dynamic allocator is available, but allows one to opt-out of this ("no_std" mode). Code written for embedded devices or baremetal contexts uniformly opt into this mode, but because it's not the default, you can't just pull any old library off the shelf and expect it to work for you, so the ecosystem is much smaller and less mature. Defaults matter.
A scene graph needs 2 mutable references, and has nothing to do with ownership. Same issue exists with GUI's. The pattern that Rust forces is to always request a reference, which incurs a performance penalty while retrieving the same reference again and again and again.
The argument is often about when ownership and borrowing is truly necessary. Rust has its uses, but arguably not all the time and with everything, because of its defaults.
Isn't the crux that Rust does those things without a garbage collector, that's the novel part? Someone correct me if I'm wrong (likely), but I think all those languages have garbage collectors, which Rust doesn't.
Studies by Microsoft and Google have already been done on this and Rust provides real tangible benefits. No one has ever claimed Rust eliminates all memory errors (if thatβs the bar youβre setting), but it makes them vanishingly unlikely, even when you include the prescience of unsafe, thus βeliminatingβ memory errors (most, not all):
> Memory safety issues, which accounted for 76% of Android vulnerabilities in 2019, and are currently 24% in 2024, well below the 70% industry norm, and continuing to drop.
The old adage is important: do not left perfect be the enemy of good.
https://security.googleblog.com/2024/09/eliminating-memory-s...
The thing about it being optional in some languages is that it's an experiment, but one that as a feature it really pays off the more code in the ecosystem is compliant to ownership tracking. For rust, it's the vast majority of it (with opt out explicitly findable..) For languages offering it optionally, it's harder to assemble the full benefit.
Better example might be statically typed languages. They were harder to use at first, but now with good type inference and features like generics, they are much more ergonomic than at first. The accessibility gap between static and dynamic languages has narrowed with time and maybe we can expect that user-friendliness of ownership will also improve like that.
That's not quite how it works in various languages. You appear to be thinking of the garbage collector as something inseparable from the language.
Both Dlang and Vlang have optional garbage collectors, that can be turned off. In the case of Vlang, none of its libraries depend on the garbage collector. Vlang offers optional (flexible) memory management, somewhat similar to Nim (but they presently don't have optional ownership).
In the case of Julia and Vlang, their optional ownership is new and experimental. Dlang's optional ownership has been around for some years now, showing that it could be done.
Dlang and Vlang allow you to choose the type of memory management (along with some other languages) that you would like to use. Vlang does it by command line flags. You can turn off garbage collection and turn on ownership.
Well, if you exclude all the bad code people have wrote, c is a safe language... See the point I'm making here?
If coders couldn't be trusted multiple times in the past, and we had to invent language level features to correct them, but they still continued to make either the same, or a new, mistakes.... Why is rust any different?
I guarantee you we will be complaining about unsafe rust in the future because rust doesnt really bring anything new to the table other than trivial cases that were easy to code in the first place. Rust brings you nothing a c coder couldn't already do in c.... They haven't solved the enduring problems of computer science, they have simply kicked the can down the road
Until you need a library that was written with the assumption of using a garbage collector.
Rust's signature achievement is memory safety without a garbage collector. It accomplishes this through a disciplined ownership system β but that system deliberately has an escape hatch: reference counting.
Every value in a Rust program has a single owner at any given time. When that owner goes out of scope, the value is dropped β memory freed, deterministically, at a known point in execution. No GC pause, no dangling pointer, no double-free. The compiler enforces all of this statically.
But some data genuinely needs to be shared: a node in a graph owned by multiple edges, a configuration object threaded through many subsystems, a callback holding a reference into surrounding state. Enter reference counting β Rc<T> and Arc<T> β which shift ownership logic from compile time into a small runtime counter.
These are not competing features. They are complementary tools with distinct trade-offs. This article unpacks both β semantics, performance, ergonomics, and the decisive question: which should you reach for?
The ownership model is Rust's core innovation. Every heap allocation has exactly one owner β the variable binding that "holds" it. Ownership can be moved to another binding, at which point the original is invalidated. It can never be silently copied (unless the type implements Copy).
fn main() { let s1 = String::from("hello"); let s2 = s1; // ownership MOVED β s1 is now invalid
// println!("{}", s1); β compile error: value borrowed after move
println!("{}", s2); // fine β s2 is the sole owner
} // s2 dropped here, memory freed automatically
The borrow checker enforces the ownership rules. When s1 is assigned to s2, the compiler understands that s1 can no longer be used β it has given up ownership. This eliminates use-after-free at zero runtime cost.
Moving ownership everywhere would be cumbersome. Rust lets you borrow a value: take a temporary, scoped reference without transferring ownership.
fn calculate_length(s: &String) -> usize { s.len() } // s goes out of scope but does NOT drop the String
fn append_world(s: &mut String) { s.push_str(", world"); }
fn main() { let mut greeting = String::from("hello");
let len = calculate\_length(&greeting); // shared borrow
append\_world(&mut greeting); // mutable borrow
println!("'{}' has {} characters", greeting, len);
}
The borrow checker enforces two invariants simultaneously:
&T) borrows may coexist β they are read-only and cannot alias a mutation.&mut T) borrow may exist at a time β and no shared borrows may co-exist with it.This is aliasing XOR mutability β a fundamental rule that eliminates entire categories of bugs (iterator invalidation, data races) statically.
References are augmented with lifetimes β annotations that prove a reference cannot outlive the data it points to. In most code the compiler infers them; in complex generic APIs you annotate explicitly.
// 'a says: the returned reference lives at least as long as both inputs fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() >= y.len() { x } else { y } }
Key Properties
Ownership & Borrowing is zero-cost: all safety guarantees are verified at compile time and produce no runtime overhead β no counter increments, no heap allocations beyond the value itself, no extra indirection.
Ownership is a strict single-owner model. But some programs require shared ownership β multiple parts of a program that each need to keep a value alive. The classic example is a graph where multiple edges point to the same node.
Rc<T> (Reference Counted) wraps a value on the heap alongside a pair of counters: a strong count (active owners) and a weak count. Every clone of the Rc increments the strong count; every drop decrements it. When the strong count reaches zero, the inner value is dropped.
use std::rc::Rc;
fn main() { let a = Rc::new(String::from("shared data")); let b = Rc::clone(&a); // increments strong count β cheap pointer clone let c = Rc::clone(&a);
println!("strong count = {}", Rc::strong\_count(&a)); // 3
drop(b);
println!("after drop b = {}", Rc::strong\_count(&a)); // 2
} // a and c drop here; count β 0 β String is freed
Rc<T> gives shared ownership but immutable access. To mutate the inner value you pair it with RefCell<T>, which moves borrow checking from compile time to runtime:
use std::rc::Rc; use std::cell::RefCell;
fn main() { let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
let clone1 = Rc::clone(&shared);
let clone2 = Rc::clone(&shared);
clone1.borrow\_mut().push(4); // runtime borrow check
clone2.borrow\_mut().push(5);
println!("{:?}", shared.borrow()); // \[1, 2, 3, 4, 5\]
}
Rc<T> is not Send or Sync β its counter is not atomic and cannot cross thread boundaries. For concurrent use, swap in Arc<T> (Atomically Reference Counted), which uses atomic CPU operations for the counter. For interior mutability across threads, pair it with Mutex<T> or RwLock<T>.
use std::sync::{Arc, Mutex}; use std::thread;
fn main() { let counter = Arc::new(Mutex::new(0u32));
let handles: Vec<\_> = (0..8).map(|\_| {
let c = Arc::clone(&counter);
thread::spawn(move || {
\*c.lock().unwrap() += 1;
})
}).collect();
for h in handles { h.join().unwrap(); }
println!("count = {}", \*counter.lock().unwrap()); // 8
}
Watch Out β Reference Cycles
Rc<T> and Arc<T> cannot automatically detect reference cycles. If two Rc values hold each other, their strong counts never reach zero and you leak memory. Use Weak<T> for back-references (e.g., parent pointers in a tree) to break cycles.
| Dimension | Ownership & Borrowing | Rc / Arc |
|---|---|---|
| Ownership model | Single owner, strictly enforced | Shared ownership via counted handles |
| Verification | Compile time β zero runtime cost | Runtime β counter ops on every clone/drop |
| Performance | Zero overhead β no extra indirection | Small overhead: heap alloc, counter, pointer deref |
| Thread safety | Enforced by Send/Sync compile-time | Rc: single-thread only; Arc: thread-safe |
| Mutation | &mut T β exclusive, statically checked | Requires RefCell/Mutex β runtime panics possible |
| Cycles | N/A β single owner can't cycle with itself | Reference cycles leak memory β use Weak |
| API complexity | Steeper learning curve (lifetimes) | Simpler surface; complexity moves to runtime |
| Typical use case | Default for almost all data in Rust | Graphs, trees with shared nodes, shared config, callbacks |
| Drop timing | Deterministic β at end of owner's scope | Deterministic β when last handle drops (may be non-obvious) |
| Cloning cost | Deep copy (or move β free) | Pointer copy + counter increment β O(1) |
The overhead of Rc<T> is three-fold: one extra heap allocation for the control block, one pointer indirection on every access, and two integer increments/decrements on clone and drop. For most programs this is negligible. For hot-path code with millions of operations per second β game engines, compilers, signal processors β preferring owned values matters.
Arc<T> adds further cost: atomic operations use CPU memory-ordering guarantees (SeqCst or Release/Acquire), which inhibit certain compiler and hardware reorderings. On contended multi-core workloads, this can become measurable.
Rule of Thumb
Reach for Rc/Arc when the alternative is fighting the borrow checker with complex lifetime annotations or unsafe code. The small runtime cost buys you ergonomic, safe shared ownership β a reasonable trade in most application code.
Arc<Mutex<T>>).// Classic use case: graph with shared nodes use std::rc::{Rc, Weak}; use std::cell::RefCell;
struct Node { value: i32, children: Vec<Rc<RefCell<Node>>>>, parent: Option<Weak<RefCell<Node>>>>, // Weak breaks parentβchild cycle }
impl Node { fn new(value: i32) -> Rc<RefCell<Node>> { Rc::new(RefCell::new(Node { value, children: vec![], parent: None, })) } }