Rust Made Me Suspicious of Convenient Abstractions

What ownership, explicit failure, and compiler friction teach when C++ muscle memory wants to move fast.

For years, C++ was my default way to speak to a machine. If I needed a fast system component, a low-latency engine, or a tight memory footprint, C++ offered a direct line to the hardware. It is a language built on trust. It trusts you to remember who owns a pointer, when a reference becomes dangling, and which threads are mutating shared state at the same time.

When you move fast in C++, you learn to rely on mental models. You establish conventions. You agree that certain functions consume data and others just borrow it. You document lifetime requirements and hope the team reads the comments. It feels productive because the compiler usually gets out of your way. If the syntax is valid, the code compiles, and you are left to debug the segmentation faults at runtime.

Then I started writing Rust.

My first few weeks with Rust were not productive. They were frustrating. My C++ muscle memory told me to share references freely, build complex graph structures, and mutate state wherever it made sense. The Rust compiler rejected almost everything I wrote. It complained about lifetimes. It complained about multiple mutable borrows. It refused to compile patterns that I had written a hundred times in C++.

At first, I blamed the language. I thought the borrow checker was an overly strict academic experiment getting in the way of real work. But as I kept fighting the compiler, my perspective shifted. Rust was not just checking my syntax. It was auditing my architecture. It was forcing me to admit that my convenient C++ abstractions were often hiding dangerous ambiguity.

The friction of ownership

In C++, ownership can be expressed with tools like std::unique_ptr and std::shared_ptr, but raw pointers and non-owning references remain easy to pass around. The language does not enforce one ownership model for every object, so a lifetime contract can still be missed.

Rust makes ownership part of the language. A value owned by a variable is dropped when it leaves scope, while a reference does not own the data. If you pass data to another function, you explicitly choose whether to move it, borrow it immutably, or borrow it mutably.

Consider a simple example of processing a string. In C++, you might pass a reference:

void process(const std::string& data) {
    // Read the data
}

std::string message = "hello";
process(message);
// message is still valid here

This looks safe, but it relies on convention. If process decides to store that reference somewhere that outlives message, the compiler will not stop you.

In Rust, the same intent requires explicit borrowing:

fn process(data: &String) {
    // Read the data
}

let message = String::from("hello");
process(&message);
// message is still valid here

The difference is that Rust checks, according to its borrow rules, that the borrow (&message) cannot outlive the owner (message). If process tries to stash that reference in a global variable, the program will not compile.

When I first encountered this, it felt like unnecessary friction. But that friction is information. If the compiler rejects a borrow, it means I have created a state where data aliasing and mutation could collide. The friction forces me to resolve the ambiguity before the code ever runs.

Data races and aliasing

In C++, passing a non-const reference to a function is trivial. You do it to avoid copying large objects or to allow the function to modify the caller’s state. But when multiple threads or complex event loops get involved, the lifetime and synchronization rules become easy to miss. If thread A holds an iterator, pointer, or reference into a vector and thread B calls push_back, a reallocation can invalidate that handle. Unsynchronized concurrent access is also a data race.

Rust approaches this by enforcing a strict rule: you can have exactly one mutable reference to a piece of data, or any number of immutable references, but never both at the same time. This is not just a thread safety rule; it is a general aliasing rule.

let mut numbers = vec![1, 2, 3];
let first = &numbers[0];
numbers.push(4); 
// Compiler error! Cannot borrow as mutable because it is also borrowed as immutable.
println!("{}", first);

In C++, the equivalent code compiles. If push reallocates and the program later uses first, that use can be undefined behavior. Rust rejects this particular overlap at compile time, which removes one class of invalidation bug from this code.

This is the friction that initially feels unnatural. You are used to the compiler trusting you to order operations correctly. In a large codebase, explicit aliasing rules are a useful safeguard, even though they do not replace sound synchronization design.

Making failure explicit

C++ gives you many ways to fail. You can throw an exception, return a null pointer, return an error code, or simply set a global errno and hope the caller checks it. This variety creates a constant cognitive load. Every time you call a function, you have to guess how it might fail and how you are supposed to handle it.

Exceptions are particularly convenient because they are invisible. You do not have to write error handling code if you do not want to. The error will just bubble up the stack until it crashes the program or hits a generic catch block.

Rust rejects the convenience of invisible errors. It uses Result and Option to represent explicit recoverable-error and absence paths, while panic! remains available for unrecoverable failures.

use std::fs::File;
use std::io::Error;

fn open_config() -> Result<File, Error> {
    let f = File::open("config.json")?;
    Ok(f)
}

The ? operator is concise error propagation. On Err, it returns from open_config instead of making the failure disappear. A returned Result is marked #[must_use], so ignoring it normally produces a warning, not an automatic compile error. Whether warnings fail the build is a project setting.

This explicitness changes how you design systems. You have to account for the success and error paths in the type. You can still discard a result deliberately, but the choice is visible in the code and can be enforced more strictly by project lint settings.

Forcing abstractions to earn their keep

In C++, it is tempting to build deep, complex abstractions. You can create hierarchies of classes, multiple inheritance trees, and layers of templates that hide the messy details of the hardware. The language gives you the tools to create an illusion of simplicity.

Rust can make complex abstractions difficult to express. If you build a deeply nested graph of objects with shared mutable state, the borrow checker may push you toward reference counting (Rc or Arc) and interior mutability (RefCell or Mutex), making the synchronization cost visible in the type signatures.

At first, I found this infuriating. I just wanted to build a simple tree structure. Why was Rust making it so hard?

The answer is that tree structures with parent pointers and shared state are not simple. They are inherently complex and dangerous in a concurrent environment. Rust was just forcing me to acknowledge the complexity I was trying to hide.

When a design leads you toward Arc<Mutex<T>> for shared mutable state, it is worth asking whether that sharing is necessary. Sometimes the better choice is message passing or flatter data, and sometimes the mutex is the right tool. The important part is that the synchronization decision is visible.

The friction of the borrow checker forces your abstractions to earn their keep. If a design is too hard to express in Rust, it may be a sign that its ownership or synchronization boundaries need another pass. The compiler often pushes you toward simpler, more linear data flow.

The value of suspicion

Writing Rust has changed how I write C++. It has made me suspicious of convenience.

When I look at a C++ codebase now, I see the invisible assumptions. I see the raw pointers that imply a lifetime contract that nobody documented. I see the shared state that is completely unprotected from race conditions. I see the functions that can throw exceptions without any indication in their signature.

Rust did not teach me that C++ is a bad language. C++ remains an incredibly powerful tool for low-level systems. But Rust taught me that power without constraint is a liability.

The friction of ownership, the strictness of lifetimes, and the explicitness of error handling are not just language features. They are a methodology. They encourage designs where some invalid states are harder to represent, ownership transfers are visible, and error paths are deliberate.

When C++ muscle memory wants to move fast, it is easy to reach for a convenient abstraction. Rust taught me to slow down, look at the memory, and ask who actually owns it. That suspicion is the most valuable thing the compiler ever taught me.

Older writing

Also read

The Most Interesting Part of a Compiler Is What It Refuses to Accept