Rust for C++ Developers: Ownership Is the RAII You Already Know
TL;DR: Rust is C++'s discipline with the compiler as enforcer. Ownership =
unique_ptrsemantics on every value. Borrowing = references the compiler proves can't dangle.Result/Optionreplace exceptions andnullptr. Traits replace inheritance. Cargo replaces CMake and the entire build-system argument. Your C++ instincts about lifetimes are exactly the right instincts — Rust just rejects the programs where they fail. Run every example below in Cubemate with zero toolchain setup.
C++ developers have the strangest experience learning Rust: the hard part — thinking about who owns memory, when it moves, and how long references live — is already in your head. You've been doing it manually for years, with valgrind and code review as your safety net. Rust's pitch is simple: those rules you already follow become compile errors when violated.
Ownership: unique_ptr semantics, everywhere, by default
In modern C++ you reach for std::unique_ptr to express single ownership. In Rust, every value works that way:
fn main() {
let s1 = String::from("cubemate");
let s2 = s1; // MOVE — like unique_ptr assignment
// println!("{}", s1); // compile error: value moved
println!("{}", s2); // fine
}
Where C++ copies by default and moves when you ask (std::move), Rust moves by default and copies only when you ask (.clone(), or automatically for cheap Copy types like integers). Use-after-move — a silent landmine in C++ — is a compile error here.
Heap allocation is explicit but managed: Box::new(value) is your make_unique, Rc<T> and Arc<T> are shared_ptr (single-threaded and atomic, respectively). delete does not exist; drops happen deterministically at scope end, exactly like the RAII destructors you already rely on.
Borrowing: references that can't dangle
C++ references and pointers trust you. Rust references are checked:
fn longest_word(text: &String) -> usize { // &T — const reference
text.split_whitespace()
.map(|w| w.len())
.max()
.unwrap_or(0)
}
fn shout(text: &mut String) { // &mut T — non-const reference
text.push_str("!!");
}
fn main() {
let mut s = String::from("hello world");
println!("{}", longest_word(&s));
shout(&mut s);
println!("{}", s);
}
The borrow checker enforces two rules, both of which you already follow in careful C++: any number of readers or exactly one writer (never both at once), and no reference may outlive its referent. The second rule kills dangling pointers at compile time; the first kills iterator invalidation — modifying a container while iterating it, legal-and-lethal in C++, does not compile in Rust.
The honest cost: for your first weeks, the borrow checker will reject programs you know are fine. Usually it's right and the program had a subtle issue; occasionally you restructure code to satisfy it. Both get dramatically rarer with practice.
Errors: Result instead of exceptions, Option instead of nullptr
Rust has no exceptions and no null. Fallibility and absence are ordinary values in the type system:
fn parse_age(input: &str) -> Result<u32, String> {
match input.trim().parse::<u32>() {
Ok(n) if n < 130 => Ok(n),
Ok(n) => Err(format!("{} is not a plausible age", n)),
Err(_) => Err(format!("'{}' is not a number", input)),
}
}
fn main() {
for input in ["42", "abc", "500"] {
match parse_age(input) {
Ok(age) => println!("age = {}", age),
Err(e) => println!("error: {}", e),
}
}
}
The compiler will not let you use a Result without addressing the error case — forgetting to check a return code, the classic C bug that exceptions were invented to fix, is structurally impossible. The ? operator propagates errors up the call stack with one character, giving you the ergonomics of exceptions without invisible control flow. Option<T> works the same way for "might not exist," which is why Rust has no null pointer dereferences — there's no null.
match itself will feel like switch finally done right: it works on any type, destructures values, and the compiler rejects non-exhaustive matches.
Traits instead of inheritance
There are no base classes. Shared behavior is expressed by traits — think C++20 concepts, or pure-virtual interfaces without the vtable-by-default cost:
trait Describe {
fn describe(&self) -> String;
}
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Describe for Circle {
fn describe(&self) -> String { format!("circle r={}", self.radius) }
}
impl Describe for Square {
fn describe(&self) -> String { format!("square s={}", self.side) }
}
fn print_all(items: &[Box<dyn Describe>]) { // dynamic dispatch — opt-in
for item in items {
println!("{}", item.describe());
}
}
fn main() {
let shapes: Vec<Box<dyn Describe>> = vec![
Box::new(Circle { radius: 2.0 }),
Box::new(Square { side: 3.0 }),
];
print_all(&shapes);
}
Generic functions with trait bounds (fn f<T: Describe>(x: T)) compile to monomorphized code like templates — but with errors at the declaration site, not forty lines of instantiation backtrace. Dynamic dispatch exists when you want it (dyn Trait) and is visible in the type when you use it.
The parts that are simply better days
- Cargo.
cargo build,cargo test,cargo add serde— dependency management, building, and testing in one tool. The CMake/vcpkg/conan deliberation is over. - No headers. Modules mean no declaration/definition split, no include guards, no order-of-include debugging.
- One compiler message style. rustc's errors tell you what's wrong, point at the exact span, and usually suggest the fix.
- Thread safety is checked.
Send/Syncare traits the compiler verifies — data races are compile errors, not heisenbugs. For a C++ developer this is the feature that eventually justifies the whole borrow-checker tax.
A realistic path in
- Map the memory model first (this guide's top half) — it's where your instincts transfer best.
- Rewrite a small C++ utility in Rust — something with real ownership decisions, like a text parser or a cache.
- When the borrow checker blocks you, ask why in Cubemate's chat with the actual error pasted in — the explanation plus a runnable fix teaches the model faster than fighting alone.
- For a structured deep-dive on one topic (lifetimes and traits deserve it), generate a Bookmate tutorial like "Rust ownership and borrowing for a C++ developer" and run every example.
Both languages run in Cubemate's sandbox — no rustup, no CMake — so the side-by-side experiments that make the mapping stick cost seconds, not an evening of setup. For the general method of switching languages efficiently, see How to Learn a Second Programming Language.
Frequently asked questions
Is Rust easy to learn for C++ developers?
Easier than for anyone else — most of Rust's famous difficulty is the ownership model, and C++ developers already think in lifetimes, moves, and RAII. The adjustment is that the compiler now enforces rules you previously followed by discipline. Expect one to two weeks to basic productivity and a few more before the borrow checker stops surprising you.
What is the Rust equivalent of C++ smart pointers?
Ownership is built into every type: a plain Rust value behaves like a unique_ptr (single owner, moved on assignment), Box<T> is an explicit heap allocation, Rc<T>/Arc<T> correspond to shared_ptr, and references (&T, &mut T) are compile-time-checked borrows that can never dangle.
Does Rust have exceptions like C++?
No. Recoverable errors return Result<T, E> and absent values return Option<T> — both are ordinary enums you handle with match or the ? operator. Unrecoverable bugs panic, which is closer to a failed assert than to a thrown exception.
Does Rust have inheritance?
No class inheritance. Rust uses traits — closer to C++ concepts or pure-virtual interfaces — plus generics with trait bounds for static dispatch and trait objects (dyn Trait) for dynamic dispatch. Composition replaces base-class hierarchies.
Can I try Rust without installing the toolchain?
Yes — Cubemate compiles and runs Rust in a browser-based sandbox, alongside C++ in the same chat, so you can run both versions of a snippet side by side with no rustup or cargo setup.
Ready to start coding?
Write, run, and learn code in your browser — Python, Java, Go, Rust and more. No installation, no setup. Free 7-day trial.
Start Free