Rust Ownership in Practice: Three Patterns That Tripped Experienced C++ Engineers
Move semantics in C++ gave engineers a false sense of familiarity with Rust ownership. The borrow checker disagrees at compile time in instructive ways.

Experienced C++ engineers arriving at Rust 1.75 on our firmware-adjacent services team assumed move semantics transferred cleanly. Three patterns caused repeated compile errors — and two caused subtle logic bugs before clippy caught them.
Pattern 1 — &mut self method called twice (the "iterator" trap)
C++ mental model:
vec.push_back(vec[0].clone()); // illegal in safe Rust anyway
for (auto& x : v) { modify(x); } // fine with care
Rust:
// ERROR: cannot borrow `v` as mutable more than once
for item in &mut v {
v.push(0); // inside loop
}
Why: exclusive borrow of whole v for iterator; push needs another &mut v.
Fix patterns:
- Collect indices first, apply after loop
split_at_mutfor known-safe partitionsVec::drainfor consume-and-append
C++ developers reach for raw pointers here — fight that; use indices or unsafe only with documented invariants.
Pattern 2 — moved value in error path (RAII vs Drop order)
C++:
Resource r = acquire();
if (fail) return; // r destructs
use(r);
Rust:
let r = acquire();
if fail { return Err(...); } // r drops — OK
let s = r.into_inner(); // move
other(s);
// using r here — compile error
Bug we shipped in review (caught in test):
let conn = pool.get()?;
if validate(&conn).is_err() {
pool.release(conn); // conn moved? partial move of fields?
return Err(...);
}
conn was moved into validate if signature took ownership. Fix: validate(&conn) or conn.clone() on cheap handle.
Lesson: Rust forces explicit move at call boundary; C++ implicit copy/move elision hid the question.
Pattern 3 — 'a lifetime of returned reference from temporary
C++:
const std::string& get() { return build().name; } // dangling — UB
Rust refuses to compile — good. But this compiles then confuses:
fn config_path() -> &str {
&format!("/etc/{}", APP_NAME) // ERROR: temporary
}
Engineers fix with String return — then fight caller expecting &str in struct:
struct Cfg { path: &str } // needs lifetime parameter on struct
Fix: owned String in struct, or Cow<'static, str>, or once_cell::sync::Lazy.
We standardize: public APIs return owned types unless profiling proves allocation hot — opposite of C++ string_view creep.
Bonus — interior mutability surprise
RefCell in C++-brain code "for convenience" caused runtime borrow panics in async task + sync callback overlap.
Rule: RefCell only in single-threaded UI glue; use Mutex/RwLock in async with clear lock order doc.
Pattern 4 — Clone as default fix
C++ engineers reach for clone() on every borrow conflict. We measured one hot path: 40% CPU in String::clone on log labels. Fix was Cow<'static, str> for static labels and Arc<str> for shared dynamic strings — requires upfront API design, not post-hoc cloning.
Clippy lint redundant_clone in CI; review overrides with benchmark justification.
Pattern 5 — Async trait objects and Send bounds
Service trait with async fn in trait (Rust 1.75 async trait stabilization partial via AFIT):
async fn fetch(&self) -> Result<Data, Error>; // may require Send bound on impl
C++ coroutine users assume thread-safe by default. Our Tokio 1.35 runtime required Send on held futures across await points — non-Send type in struct failed at compile time in one crate, at runtime block_on panic in another during migration.
Standardize on async-trait crate 0.1 for dyn compatibility until native async traits cover our MSRV.
Code review prompts for C++ migrants
Reviewers ask: "Where does ownership transfer?" and "Why is this not Arc?" on every PR for first 90 days. Sounds heavy; shortened ramp for two senior hires who skipped the prompts and reintroduced data races in async HTTP client.
We assign a Rust buddy (not mentor — peer) for first month on any service crate touching Send/Sync boundaries.
Internal rust-patterns crate documents approved Arc<Mutex<T>> lock ordering for shared connection pools — copy-paste forbidden; import the wrapper type instead.
Read The Rustonomicon chapter on aliasing before first unsafe review — C++ migrants underestimate stacked borrow rules in FFI shims.
Our MSRV policy (1.75 today) lags latest Rust one edition cycle — document which async/ownership features are allowed in shared crates vs experimental branches.
Team onboarding changes
- No C++ analogies in code review — explain the borrow, not move ctor
- Mandatory clippy::pedantic on CI —
rustc 1.75,-D warnings - Pair on first
Pin/Futureexposure — C++ coroutines experience helps here, ownership does not
Embedded overlap
DMA buffers and 'static lifetimes intersect no_std DMA notes — ownership errors become hardware bugs.
Compare with C undefined behavior on Cortex-M where the compiler "fixes" what Rust rejects at compile time.
What I'd do next
Internal mini-crate borrow_patterns with compile_fail doctests — # trybuild tests for the three antipatterns.
Measure whether Arc clone churn from refusing &str in APIs costs us — one hot path in metrics aggregator; if so, document exception with benchmark.
Rust 2024 edition async traits stabilization — revisit our service trait objects after upgrade.
Manish Bookreader
Electronics enthusiast, Embedded Systems Expert, Linux/Networking programmer, and Software Engineer passionate about AI, electronics, books, and cooking.

