I've been writing systems-level code for a long time. C for embedded work, C++ for anything that needed RAII or templates. When Rust started gaining serious traction I was sceptical — yet another language promising to fix C without the costs. After shipping production Rust for a while now, here's an honest account of where it delivers and where the tradeoffs are real.

What Rust Actually Solves

The headline promise is memory safety without a garbage collector, and it delivers. The borrow checker enforces at compile time what C++ programmers enforce through discipline, code review, and sanitiser runs. Use-after-free, double-free, data races — these are a class of bugs that simply don't exist in safe Rust. Not "are rare". Don't exist.

In C++ you learn to manage lifetimes through idioms: RAII, unique_ptr, shared_ptr, careful pass-by-value vs. pass-by-reference decisions. It works, but the rules live in your head and in your team's conventions. In Rust the compiler holds those rules. A junior developer can't accidentally introduce a data race; the code won't compile.

This matters most in concurrent code. Writing lock-free data structures or message-passing systems in C++ is expert territory — the kind of work where even experienced engineers reach for thread sanitiser and pray. In Rust, Send and Sync are part of the type system. If you pass something across a thread boundary that isn't safe to do so, the compiler tells you on the spot.

The Borrow Checker: Obstacle or Teacher?

The honest answer is: both, in sequence.

The first few weeks with Rust feel like arguing with a pedantic colleague who keeps pointing out that your perfectly-sensible C++ would be undefined behaviour. They're right, but it's annoying. You reach for patterns that work in C++ — a mutable reference here, a raw pointer shortcut there — and the compiler refuses.

Then something shifts. You start structuring data differently. Instead of a graph of mutually-referencing objects, you think in terms of ownership hierarchies, arenas, indices into a Vec. The borrow checker isn't blocking you; it's pushing you toward designs that are easier to reason about. The code that eventually compiles is usually better than the code you started with.

C++ programmers who stay frustrated with Rust long-term are often trying to write C++ in Rust syntax. The language rewards different patterns.

Where C and C++ Still Win

Absolute control. Rust gives you unsafe blocks, but the default is safe. When you need to hand-roll an allocator, do platform-specific memory mapping, or interface with hardware at the register level, C and C++ impose no friction. In Rust you're writing unsafe and documenting invariants and hoping you got it right — which is the same position as C, but with more ceremony.

Compile times. Rust's incremental compilation has improved significantly, but a large Rust codebase still compiles more slowly than equivalent C++. If you're iterating tightly in a real-time or embedded context with slow hardware-in-the-loop feedback, this matters.

Ecosystem maturity. C++ has decades of battle-tested libraries for signal processing, numerical computing, game engines, embedded runtimes. Rust's ecosystem is growing fast but there are still gaps, and crate quality varies. Wrapping a C library with bindgen works, but it's friction.

Legacy integration. Most existing systems code is C or C++. Calling into C from Rust via FFI is straightforward; the reverse is more work. Migrating a large C++ codebase is a long-term project, not a flag you flip.

The Things Nobody Talks About

Result and ? are a genuine improvement over exceptions and error codes. C++ exceptions are expensive when thrown and invisible in function signatures. Error codes require discipline to check. Rust's Result<T, E> with the ? operator makes the happy path clean and forces you to acknowledge fallibility at every call site. After using this for a while, going back to errno or unchecked exception propagation feels regressive.

The tooling is excellent from day one. cargo handles dependencies, building, testing, benchmarking, and documentation. Setting up a C++ project with equivalent coverage — a package manager, a test harness, a doc generator, a formatter — involves composing half a dozen tools that don't always agree. Rust ships that stack in one command.

The community writes documentation. The Rust ecosystem has a culture of writing /// doc comments and publishing them on docs.rs. This sounds minor until you've spent an afternoon reading a C library's header file trying to figure out ownership semantics.

Which Should You Choose?

For new systems projects where memory safety and concurrency are concerns and you have the time to learn the language properly: Rust. The upfront investment pays back in fewer production incidents.

For embedded or real-time work where every byte and cycle is accounted for and you're working close to hardware: C, possibly with a future migration path to Rust once embedded support matures further.

For large existing C++ codebases: the realistic answer is C++ with better tooling — sanitisers, static analysis, modern C++20/23 features. A full rewrite is rarely justified by safety alone; incremental Rust adoption at the boundaries is more practical.

For learning systems programming: start with C to understand what the machine actually does, then learn Rust to understand what the language can guarantee about it. The contrast is educational in both directions.


Rust didn't make me a better programmer by solving problems for me. It made me a better programmer by refusing to let me get away with the things I used to get away with.

← Back