July 12, 2026 · 12 min read
Rust Notes: Safety, Performance, and Trade-offs
A C/C++-oriented look at Rust ownership, zero-cost abstractions, Cargo, and the cost of learning and using the language.
Rust Is Showing Up in More Places
Over the past few years, I have kept seeing Rust in announcements about rebuilt infrastructure components and frameworks. When people talked about systems programming in the past, the default answer was C or C++. Rust is now hard to ignore.
As of July 2026, Rust has entered the TIOBE Index top 10 for the first time, at number 10. A ranking cannot make a technology choice for a project, but this is still a useful signal: Rust is no longer just a new language that looks interesting. It is becoming a real option for more teams.
My reason for learning Rust was simple: I wanted to add another piece to my C/C++ skill set. I am not especially interested in whether it will replace C++. What drew me in was that Rust puts many problems that normally depend on experience into language rules and compiler checks.
The First Thing You Cannot Avoid: Ownership
The frustrating part of writing Rust is often not the syntax. It is that code you would write without much thought in another language can be rejected immediately.
let name = String::from("Rust");
let moved_name = name;
// println!("{name}"); // Does not compile: ownership of name has moved.
println!("{moved_name}");
In C++, I first need to decide whether this is a copy, a reference, or a std::move. In Java or Dart, variables are usually object references. Rust puts a more direct question in front of me: who owns this String now, and what is the original name still allowed to do?
This kind of error is easy to find annoying. It is only an assignment, so why is the compiler being so strict? But it is checking who is responsible for releasing the resource. name and moved_name cannot both believe they own the same allocation, which removes one path to double frees and dangling pointers.
The equivalent ownership transfer is also clear with C++ std::unique_ptr:
auto name = std::make_unique<std::string>("Rust");
auto moved_name = std::move(name);
std::cout << name->size(); // Compiles, but name is now empty. Dereferencing it is undefined behavior.
C++ has transferred ownership to moved_name, but its type system does not stop me from dereferencing name again. unique_ptr is an excellent RAII tool; the issue is that developers still need to remember not to use the moved-from object in its old way. Rust turns that convention into a compile error.
Borrowing rules can be even easier to get stuck on: at any moment, there can be several read-only references, or one mutable reference, but not both.
let mut title = String::from("Rust");
let editing = &mut title;
editing.push_str(" Book");
// While editing is in use, title cannot be borrowed again.
These rules do add friction to application code, especially when data crosses function, async-task, or thread boundaries. Some designs cannot be fixed by simply passing a reference down; ownership and task boundaries need to be reconsidered.
That is also why I value Rust. In C and C++, many resource problems are caught through conventions, review, ASan, TSan, and tests. Rust moves part of that work into compilation. Before the code runs, the compiler is already asking: can this reference dangle? Is there shared mutable state? Will this value still exist after it leaves this scope?
The difference is even clearer with concurrency. This C++ snippet looks ordinary, but the two threads write to counter concurrently and create a data race:
int counter = 0;
std::thread first([&] { ++counter; });
std::thread second([&] { ++counter; });
first.join();
second.join();
Rust will not let ordinary mutable data be passed directly into two threads. Sharing requires the synchronization mechanism to be explicit:
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let worker_counter = Arc::clone(&counter);
let worker = thread::spawn(move || {
*worker_counter.lock().unwrap() += 1;
});
*counter.lock().unwrap() += 1;
worker.join().unwrap();
This does not mean Rust solves concurrency automatically. A Mutex can still be used badly, and lock granularity and deadlocks remain engineering problems. But Arc makes shared ownership across threads explicit, and Mutex makes mutable access mutually exclusive. Remove either one, and the code will not compile. Compared with protecting a shared variable by convention, the boundary is much harder to miss.
Rust is not absolutely safe just because it is Rust. unsafe, FFI, deadlocks, logic errors, and resource exhaustion still exist. But in ordinary safe Rust, this layer of constraints means memory problems no longer depend entirely on developers remembering to be careful.
Performance Does Not Have to Mean Writing Everything at the Lowest Level
Rust can be discussed alongside C and C++ on performance partly because it has no garbage collector. The lifetime of most objects can be determined at compile time. Resources are released when they leave scope rather than being found and collected by a runtime, as in Java, Go, or Dart.
Swift and Objective-C take a different route with ARC, using reference counting to manage object lifetimes. That is often a good engineering experience, but frequent retain/release operations also have a cost. Every language has a suitable place; I do not think a language should be ranked solely by whether it has GC. Rust’s approach is simply compelling for latency-sensitive and memory-constrained work.
Another term that comes up often is zero-cost abstraction. Iterators, closures, and generics are more pleasant to use than handwritten loops, but they do not necessarily add runtime overhead. Rust’s mainstream toolchain uses an LLVM backend for inlining, monomorphization, and dead-code elimination, so high-level code can often become machine code close to a handwritten low-level version.
For example, this Rust code filters, transforms, and sums values:
let total: u64 = values
.iter()
.filter(|&&value| value % 2 == 0)
.map(|&value| u64::from(value) * u64::from(value))
.sum();
In C++, many developers would write the equivalent as a loop:
std::uint64_t total = 0;
for (const auto value : values) {
if (value % 2 == 0) {
const auto number = static_cast<std::uint64_t>(value);
total += number * number;
}
}
Both express the same computation. In an optimized build, Rust iterator chains are commonly expanded and fused, so you do not need to fall back to a handwritten loop simply to avoid intermediate collections or virtual calls. The final answer should still come from benchmarks and profiles, not a language’s marketing page.
That fits the way I like to work. I do not have to start with a screen full of pointers and templates just to keep code fast. When a hot path matters, I can still inspect assembly, memory layout, and real profiles. Performance always needs measurement; algorithms, I/O, lock contention, and databases often matter more than the language itself. Rust simply does not begin by placing a heavy runtime cost in that path.
Cargo Saves Me From Spending So Much Time on Setup
The benefit of Cargo is much easier to notice than ownership.
C/C++ dependency management has always been one of the more painful parts for me. CMake and vcpkg work, and they keep improving, but every project combines them differently. Is the library source code, a static library, or a dynamic one? How should the include and link paths be configured? Will arm64, x86_64, or CI behave differently? It is easy to spend half a day on the environment before writing the actual feature.
Rust has a complete default path: Cargo.toml manages dependencies, Cargo.lock pins versions, and creating a project, fetching crates, testing, and producing a release build all start from the same tool.
cargo new rust-notes
cd rust-notes
cargo add serde
cargo test
cargo build --release
Dependency declarations do not require treating package management and build scripts as two separate mental models either:
# Cargo.toml
[dependencies]
serde = { version = "1", features = ["derive"] }
The same C++ dependency in a common vcpkg + CMake setup lives in at least a manifest and a build script:
// vcpkg.json
{
"dependencies": ["fmt"]
}
# CMakeLists.txt
find_package(fmt CONFIG REQUIRED)
target_link_libraries(app PRIVATE fmt::fmt)
This is not to say CMake or vcpkg cannot do the job. They are common in real C++ projects and can handle complex cases. Cargo simply keeps “what are the dependencies, how do I build, and how do I test?” inside one default workflow, which leaves fewer decisions at the start of a project.
Cargo is not magic when system libraries, cross-compilation, or native bindings are involved. Environment differences still happen. But a pure Rust project can get from zero to running smoothly without bouncing between CMake, a package manager, and IDE settings.
By comparison, C++ shows more of its historical baggage. C++26 continues to evolve, but C++11 and even older standards are still common in real projects. Modules, ABI, compiler options for third-party libraries, and platform differences all make large integrations worth handling carefully. Cargo’s consistency stands out here.
A Strict Compiler Comes With Real Costs
Rust’s compiler is strict. That criticism is completely fair.
Change one line and it can trigger a chain of errors: conflicting borrows, lifetimes that are too short, missing traits, or data that cannot safely cross a thread boundary. It is easy to end up thinking, “I know what I want to do. Why will it not let me write it?” Ownership and lifetimes are not concepts that become intuitive after reading one definition; they need to be worked through in code.
But Rust error messages are usually useful. They identify which borrow has not ended, which value moved, where a clone could be added, or where a reference may make sense. They do not make architecture decisions for me, but they often narrow the problem down to the two or three lines that actually conflict.
Concurrency makes the difference especially tangible. C++ can absolutely have correct concurrent code, but data races are usually prevented by discipline. Rust uses its types and borrowing relationships to close off some invalid paths: data that cannot safely cross threads will not easily be sent into one. To satisfy the compiler, sometimes the answer is a lock, sometimes message passing, and sometimes a redesigned data structure. None of that comes for free.
Compile time is another real cost. Rust’s compiler performs type checking, borrow checking, and optimization, so large builds are not necessarily quick. Its learning curve and build speed are two concrete costs of the language.
Flutter and Rust Together
Rust is growing quickly in systems programming, infrastructure, and performance-sensitive core modules. GUI, cross-platform desktop applications, and complete web products still need to be judged by their actual requirements. A top-10 ranking is not a reason to rewrite every project.
The combination I find interesting is Flutter plus Rust. Flutter can continue to handle UI, state, and platform interaction, while Rust takes on image processing, audio/video, cryptography, protocol work, or other core logic with higher requirements for performance and resource safety.
For example, pure image computation can stay in Rust:
pub fn average_luma(pixels: &[u8]) -> u8 {
let total: u64 = pixels.iter().map(|&pixel| u64::from(pixel)).sum();
(total / pixels.len().max(1) as u64) as u8
}
On the Flutter side, the UI only needs to call it as an asynchronous capability. It does not need to know about pointers, memory layout, or the specific algorithm:
final luma = await imageCore.averageLuma(pixels);
setState(() => previewLuma = luma);
Here, imageCore could be a MethodChannel, FFI, or a generated Dart API from a bridge. Those are integration details. Keeping the core function as pure input and output is the valuable part of putting Rust at this layer.
This is close to how I think about a cross-platform native core: stable and independently testable computations and business semantics belong in the core. Pages, platform lifecycles, permissions, and file paths still belong at the platform layer. Rust can be an implementation choice for the core, but only when the boundary is clear—not when it adds another bridge just for the sake of using Rust.
A Few Learning Suggestions
Rust feels like it fills in a mental model that I was never previously forced to build.
Who owns the data? Who can change it? When is it released? Can it cross threads? These questions still matter when writing C++ or Flutter. Rust simply does not let me postpone them until after a bug appears.
For getting started, I recommend The Rust Programming Language, 2nd Edition. After reading it, I found it especially suitable for beginners: it does not begin by throwing you into framework code. It explains why ownership, borrowing, and lifetimes exist. Once those parts are understood, async, concurrency, FFI, and concrete frameworks become much easier to approach.
When writing code, the official The Rust Programming Language and The Cargo Book are also worth keeping open.
Conclusion
After completing Rust’s fundamentals, my biggest takeaway is not how much new syntax I learned. It is that many questions I used to gloss over can no longer stay vague.
Who releases this object in the end? Can this data really be mutated at the same time? If I send it into an async task or another thread, does the original logic still hold? Those questions matter in C++ too, of course, but they are often discussed in earnest during code review, testing, or only after something goes wrong. Rust puts them in front of you while you are writing the code.
That can be frustrating. A piece of logic that looked simple can require splitting data, adding Arc<Mutex<_>>, or rethinking how a task passes data because its borrowing relationships are wrong. Slow builds can also make you miss the days when every small edit ran immediately. Rust has not made complexity disappear. It refuses to leave it quietly for runtime.
That is why I do not see Rust as a replacement for C++, and I would not add it to every project because it is popular. C++ still has a huge amount of mature engineering work, libraries, and valid use cases. Rust’s GUI, cross-platform, and team ecosystem also need to be evaluated in context.
But when code really handles resources, concurrency, or performance-sensitive core logic, I am willing to spend more time letting the compiler be picky. Before the code reaches production, someone—even if that someone is the compiler—will make me think those boundaries through. That is what I value most about Rust.