The Code Reviewer Who Never Goes Home: Why C++ Engineers Can Learn Rust
Rust feels awkward to C++ engineers, but most of what seems strange is a rule they already enforce by hand. What changes is who does the enforcing.
In 2006, a 29-year-old programmer named Graydon Hoare came home to his apartment building in Vancouver and found the elevator out of order. He lived on the 21st floor. The elevator’s software had crashed, and it had crashed before. Climbing the stairs, by his own later account, he thought: “It’s ridiculous that we computer people couldn’t even make an elevator that works without crashing!”
Hoare worked at Mozilla at the time. That year he started a small language of his own in his spare time and named it after a fungus. Mozilla began sponsoring it in 2009. On May 15, 2015, Rust 1.0 shipped.
Most C++ engineers come to Rust carrying years of habits and a vague grudge. The first week feels like moving into a new apartment where every light switch sits a few centimeters from where your hand expects it. Nothing is hard. Everything is slightly off.
Look closer, though, and most of what feels strange is stuff a C++ engineer already knows.

AI-generated image
The rule we used to enforce by hand
In the 1980s, Bjarne Stroustrup gave C++ a habit with an awkward name: RAII (Resource Acquisition Is Initialization). Nobody can guess what it means from the name. What it does fits in one line: whoever uses it cleans it up. An object grabs a resource when it’s created and gives it back in its destructor when it goes out of scope. The scope is the stretch of code where a variable lives, usually one pair of curly braces. The destructor is a cleanup function that runs automatically the moment the object disappears. Open files, locks that keep two threads from touching the same data at once, network connections, memory: all handled this way. If you’ve written C++ for a living, you’ve leaned on this so often you’ve stopped noticing it.
In 2011, C++11 added std::unique_ptr and std::move. A unique_ptr is a pointer, a variable holding a memory address, that also acts as the owner of that memory and frees it when it goes away. std::move hands that ownership to another variable. Now a piece of memory could have exactly one owner, and ownership could be handed from one variable to another. Good C++ teams built whole style guides around this. One owner. Move it, don’t copy it. Don’t touch it after you’ve moved it.
The trouble is that last rule lived in our heads. The compiler was happy to let you break it:
auto a = std::make_unique<Packet>();
auto b = std::move(a);
a->send(); // compiles fine; a is now an empty (null) pointer
That line compiles. On a good day it crashes in testing. On a bad day it crashes at a customer site, and someone boards a plane with a laptop, headed for a very angry customer. The only thing standing between that line and production was a human code reviewer who happened to be paying attention that afternoon.
Here’s the same mistake in Rust:
let a = Packet::new();
let b = a;
a.send(); // error[E0382]: borrow of moved value: `a`
The program never gets built. The compiler’s message means “you tried to use a after its value moved somewhere else.” Rust’s ownership rules are the unique_ptr discipline we already preached, written into the language and checked on every single compile. The borrow checker is a code reviewer who reads every line, never takes lunch, and never says “looks good to me” (LGTM, the usual way to approve a review) because it’s Friday.
That’s the reason C++ engineers adapt. The concepts are familiar. What changes is who does the enforcing.
Reading Rust with a C++ brain
Most of the concepts that trip up Rust beginners have a C++ twin. Here’s the cheat sheet worth keeping open for the first few weeks.
Assignment is a move. In Rust, let b = a; means auto b = std::move(a);. The only difference is that you don’t have to write std::move; moving is the default. If you’ve typed std::move ten times in one C++ file, Rust is the language that types those ten for you. Cheap types like integers and f64 carry the Copy trait and just get copied, the way trivially copyable types do in C++, meaning simple types whose bytes can be copied as-is. When you want a deep copy, one that duplicates everything the value points to, you call .clone() yourself. C++ automatically calls a function named the copy constructor whenever an object is copied. In Rust, nothing like that happens behind your back.
let s = String::from("packet");
let t = s.clone(); // an explicit copy constructor call
let u = s; // auto u = std::move(s);
let n = 42;
let m = n; // i32 is Copy, so n is still usable
Const is the default. In C++, const marks a value that can’t change once set, and auto lets the compiler work out the type. Rust’s let x is const auto x, and let mut x is plain auto x. Flip the habit of adding const and you’re there. Count how many times you’ve written “missing const here” in a C++ code review, and Rust’s choice of default starts to make sense.
Borrowing just means taking a reference. In Rust, making a reference with & is called borrowing. The owner keeps the value; you only hand out permission to use it for a while. In C++ terms, it’s passing a const Packet& into a function. In C++, & makes a reference, an alias that points at the original instead of copying it, and const means the alias can’t be used to change it. The callee can read it, but it can’t delete it or walk off with it, and when the function returns the value is still sitting with its owner. Side by side as function signatures:
fn checksum(p: &Packet) -> u32 { /* ... */ } // borrow to read: uint32_t checksum(const Packet& p)
fn stamp(p: &mut Packet) { /* ... */ } // borrow to modify: void stamp(Packet& p)
fn send(p: Packet) { /* ... */ } // take ownership: void send(Packet p) + std::move
let mut pkt = Packet::new();
checksum(&pkt); // lent out, handed back
stamp(&mut pkt); // lent out for editing, handed back
send(pkt); // ownership given away; pkt is unusable from here on
Nothing new for a C++ developer so far. The difference is that the promises C++ kept by convention, Rust checks as rules. There are three. A borrow can’t outlive its owner. While something is borrowed, the owner can’t move or modify it. And the exclusivity rule below.
&mut is the restroom key. &T is const T& and &mut T is T&. At any moment you can hold as many &T as you like, or exactly one &mut T and no &T at all. There’s only one key, and while someone has it, everyone else waits outside. Think of it as a reader-writer lock taken at compile time: any number of readers may come in together, but a writer gets the room alone. C++ developers know better than anyone what this rule prevents:
std::vector<int> v{1, 2, 3};
const int& first = v[0];
v.push_back(4); // if the data moves to a new block, first points into freed memory
std::cout << first; // compiles fine; undefined behavior
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4); // error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
println!("{first}");
For readers who don’t write C++: C++ calls a data structure that holds a collection of values, like std::vector (a growable array), a container. Iterator invalidation is what happens when a reference, pointer or iterator into a container stops being valid because the container changed underneath it. A std::vector keeps its elements in one contiguous block of memory. When that block is full and you call push_back, the vector grabs a bigger block, moves every element over, and frees the old one. The data has moved house. But first is still holding the old address. Go knocking there and nobody knows what will answer. Usually the old value is still lying around, so everything looks fine, until one day you read garbage or the program crashes. The C++ compiler won’t stop you. The C++ standard calls this undefined behavior (UB), which means the language takes no responsibility for whatever happens next.
Anyone who has lost a night to iterator invalidation will understand right away why the borrow checker is so picky.
Lifetime annotations are comments that compile. The scariest-looking syntax for newcomers is 'a. If you’ve ever written a comment on a C++ function saying “the returned reference must not outlive a or b,” a lifetime annotation is that comment, written so the compiler can read it.
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
In C++, pass a temporary, a nameless value that vanishes at the end of the statement, to the same function and you quietly get a dangling reference, one that points at something already gone. Rust catches it at the call site. And most of the time the compiler infers lifetimes on its own, so you write them less often than you’d fear.
Smart pointers just changed their names. A smart pointer points at memory and also frees it once nobody needs it. You thought you were learning a new language. Here, you’re just swapping name tags.
Box<T>→std::unique_ptr<T>Arc<T>→std::shared_ptr<T>. Several owners share it; it keeps a count of how many (the reference count) and frees the memory when the count hits zero. The count is updated atomically, so threads (streams of execution running at the same time) can share it safely.Rc<T>→ a lightershared_ptrwithout the atomic counting. Faster, but single-threaded only, and the compiler stops you if you try to send one to another thread. C++ has no equivalent.Weak<T>→std::weak_ptr<T>, a pointer that looks without counting toward ownership- the
Droptrait → a destructor
The rest of the map.
Vec<T>→std::vector<T>,String→std::string&str→std::string_view,&[T]→std::span<T>: windows onto part of the data, without copying itOption<T>→std::optional<T>(maybe a value, maybe nothing),Result<T, E>→std::expected<T, E>(a value or an error, C++23)enumwith data plusmatch→std::variant(a box holding one of several types) plusstd::visit(code that branches on which type is inside), except in Rust a missed case won’t compile- traits and generics → templates and C++20 concepts. A template is code with the type left as a blank; a concept is the list of conditions a type must meet to fill that blank. Like templates, Rust generics are stamped out separately for each type (monomorphization).
dyn Trait→ virtual functions, where the program picks which function to call at run time based on the object’s actual type. C++ stores a pointer to a table of function addresses (the vtable) inside each object; Rust carries it next to the reference instead (a fat pointer).unsafe→ all the C++ you’ve ever written. Anunsafeblock switches off the compiler’s safety checks and puts the programmer on the hook. It’s the old hometown, except now there’s a sign on the door: from here on, you’re responsible.
? instead of exceptions. In C++, an exception is a mechanism that, when something fails, climbs back up the chain of function calls looking for code to handle it. Rust has no exceptions. A function that can fail returns a Result, and the caller passes the error upward with a single ?. It’s the C-style if (!ok) return err; on every line, folded into one character. One question mark stands in for hundreds of those lines. C programmers may get a little emotional here.
fn load(path: &str) -> Result<Config, std::io::Error> {
let text = std::fs::read_to_string(path)?; // returns early on failure
Ok(parse(&text))
}
Keep this map handy, and most of the compiler errors in your first few weeks turn into “oh, that.”
Two numbers from Google
In 2019, memory safety bugs made up 76% of Android’s security vulnerabilities. Google started writing new Android code in memory-safe languages, Rust among them. By 2024 the share had dropped to 24%.
In 2022, Google surveyed more than 1,000 of its own developers who had written and committed Rust code. Only about 13% had used Rust before. More than two-thirds said they felt confident contributing to a Rust codebase within two months or less. A pie chart in the write-up breaks it down: 27% got there in two to three weeks, another 39.8% in one to two months.
Two months, in other words, to stop reaching for the old light switches.
The parts that stay awkward
The same survey also exposed Rust’s sore spots. Slow builds were by far the top complaint, and only a little over 40% of respondents found build speed acceptable. Ownership and borrowing showed up on the list of the hardest topics, next to macros (code that writes code) and async (asynchronous programming).
So the borrow checker is a fair reviewer, but a slow and stubborn one. It will reject code that’s actually fine, because it can’t prove it’s fine. Any C++ engineer who has worked under a strict senior reviewer knows that feeling. You rewrite the thing, grumbling, and a few months later you notice you write it that way the first time.
Doubly linked lists are the famous sore spot. Each element points to both its neighbors, so every element is held by two others at once, which collides head-on with Rust’s one-owner rule. In C++ you write one in an afternoon. In Rust you either reach for Rc and RefCell (a box that checks the borrowing rules while the program runs), or open an unsafe block and take responsibility yourself, which is exactly what you were doing in C++ all along. The difference is that Rust makes you put up a sign that says so.
The man who built the house
On February 7, 2025, Stroustrup sent a note to WG21, the committee that steers C++. He called it “a call to urgent action partly in response to unprecedented, serious attacks on C++.” His answer is a framework called Profiles, a set of rules a compiler can enforce to make C++ type-safe and resource-safe: no reading memory as the wrong type, no using a resource after it’s been given back.
Read that description again with Rust in mind. The man who gave us RAII is trying to get a compiler to enforce what RAII always asked of us. It’s the same idea Hoare had on the stairs in Vancouver, arriving from the other direction.
Don’t get me wrong. I do respect Dr. Stroustrup. Every rule the borrow checker enforces on me started as a habit his language taught me, and a man who is still pushing his own language to be safer, more than forty years in, deserves better than a smug blog post.
Still, I can’t help wondering. If C++ gets its own borrow checker someday, will we call it modern C++, or will we finally admit we were writing Rust by hand the whole time?
References
- Clive Thompson, “How Rust went from a side project to the world’s most-loved programming language,” MIT Technology Review, February 14, 2023 (via CACM). https://cacmb4.acm.org/news/270144-how-rust-went-from-a-side-project-to-the-worlds-most-loved-programming-language
- “Rust (programming language),” Wikipedia. https://en.wikipedia.org/wiki/Rust_(programming_language)
- Google Open Source Blog, “Rust fact vs. fiction: 5 Insights from Google’s Rust journey in 2022,” June 2023. https://opensource.googleblog.com/2023/06/rust-fact-vs-fiction-5-insights-from-googles-rust-journey-2022.html
- Jeff Vander Stoep and Alex Rebert, “Eliminating Memory Safety Vulnerabilities at the Source,” Google Security Blog, September 25, 2024. https://security.googleblog.com/2024/09/eliminating-memory-safety-vulnerabilities-Android.html
- TechRadar, “Google hails move to Rust for huge drop in memory vulnerabilities.” https://www.techradar.com/pro/google-hails-move-to-rust-for-huge-drop-in-memory-vulnerabilities
- The Register, “C++ creator calls for help to defend programming language from ‘serious attacks’,” March 2, 2025. https://www.theregister.com/2025/03/02/c_creator_calls_for_action/