Rust vs C++: Values, Ownership and RAII¶
Rust and modern C++ both support resource management through values whose cleanup is tied to scope. The important difference is not “Rust uses RAII, C++ uses manual deletion”: it is which ownership and access obligations the type system expresses, and which obligations remain in the implementation or caller's contract.
Before you start¶
Read ownership, borrowing and Drop. For the comparison, basic C++ classes and automatic storage duration are enough. Use a stable Rust toolchain with edition 2024 and a C++20 compiler. The examples use only standard libraries and invented readings, not a file, lock, GPIO pin or sensor.
This begins the separate advanced series after the completed language course. First compare one ownership requirement; later lessons develop borrowing, moves and design alternatives.
Requirement: one owner, cleanup on either return path¶
Imagine an operation obtains a short-lived lease. Its owner must release it when the function returns, including when input is missing. We model acquisition and release with counters so tests can observe the protocol without depending on hardware.
The required outcomes are:
- A present reading is returned unchanged, including zero and negative fixtures.
- A missing reading is reported as absent/error, not replaced with zero.
- Each completed acquisition is matched by one release on these normal return paths.
- Copying the guard must not accidentally create a second owner of the same lease.
This model tests scope behaviour, not an actual resource's release implementation. Counters have small bounded fixture values; their arithmetic is not a production overflow policy.
C++ RAII: a guard owns the cleanup responsibility¶
Save this complete program as main.cpp:
Compile and run with assertions enabled:
Do not add -DNDEBUG to these checks: it removes the assertions, and some fixture calls appear inside assertions. Optimisation alone is not the same as disabling them.
The guard is an automatic object, not a heap allocation. Its destructor runs when run leaves the scope on either shown return path. We explicitly delete copying and moving for this example; C++ does not infer our lease-ownership policy from the reference member. An idiomatic movable resource wrapper needs its own correct transfer contract, which the move lesson will examine.
The C++ working draft's declaration statements describe scope exit and automatic objects. Destructors describe cleanup of an object's members after its destructor body. These are core mechanisms available in the tested C++20 example; the living draft is not the published C++20 standard.
Rust ownership: a Drop value guards the same operation¶
Create a separate package:
Replace src/main.rs with this complete program:
Run cargo check, cargo fmt --check, cargo test, cargo test --release, cargo run --quiet and cargo run --release --quiet. Expect four passing Rust tests. Both languages' programs produce:
? returns an error on the missing path while the guard still receives ordinary scope cleanup. _lease is a named binding that owns a value; let _ = Lease::new(trace) would instead discard that temporary immediately. A leading underscore suppresses an unused-binding warning; it is not an ownership escape.
Cell allows these small counters to change through a shared reference on one thread. This is deliberate interior mutability, not a translation of every C++ reference into &mut. The borrowing comparison will separate exclusive access from mutable state.
What the two mechanisms do not equate¶
| Question | C++ example | Rust example |
|---|---|---|
| Where does the guard live? | Automatic local object | Owned local binding |
| Who releases the lease? | The guard's destructor | The guard's Drop implementation |
| Why is duplicate ownership prevented here? | Copy operations explicitly deleted | No Copy or Clone implementation; assignment moves this value |
| Who keeps the trace valid? | Caller must respect the reference lifetime | Borrowed field carries a checked lifetime relationship in safe Rust |
| Can cleanup still be implemented incorrectly? | Yes | Yes |
The Rust destructor rules separate the Drop implementation from automatic field cleanup and define drop scopes. Rust struct fields are dropped in declaration order; C++ members are destroyed in reverse construction order. Do not mechanically port a structure whose members depend on one another's destruction order. Locals in these examples do not exercise that distinction.
Neither “owns a value” nor RAII means “must live on the heap.” Standard owning containers and smart pointers manage particular resources; ordinary scalar values usually need no custom destructor. C++ unique_ptr also expresses exclusive ownership and rejects copying. Do not compare idiomatic safe Rust only with unmanaged C++ pointers.
Compile rejection is a separate test¶
Save the working Rust source, then use this independent, deliberately invalid program:
cargo check rejects use of the moved value with E0382 on the verified compiler. Restore the working source afterward. The presence of Drop is not necessary for this move rule: the owned String already makes this wrapper non-Copy.
For a corresponding C++ ownership check, compile this separate invalid program as failure.cpp:
It must fail because copying unique_ptr is deleted. These two rejections prevent different invalid operations; they do not establish that all Rust and C++ assignments have identical semantics. In particular, Rust's rejected use of first is not the same thing as C++'s specified moved-from state after a successful move.
Change the requirement: release can fail¶
A real resource might require flushing data, sending a protocol message or committing work, any of which can fail. A destructor has no ordinary return value through which to report success to the original caller. Do not silently turn a required commit into “best effort on drop.”
Use an explicit operation returning a result when the caller must observe failure; keep cleanup as a non-throwing/non-panicking fallback where practical. Define whether that operation consumes the owner, what happens after failure, and whether retry is allowed. The error-contract and construction lessons will build on that distinction.
Also, cleanup is not an unconditional execution guarantee. Process abort, abrupt termination and intentional ownership leaks are outside this example's normal-return contract. Rust's safe mem::forget demonstrates why unsafe correctness must not depend on destructors always running. C++ leaks and shared-ownership cycles can likewise prevent intended reclamation. Ownership does not prove absence of deadlocks, resource exhaustion or application errors.
Exercises and selection criteria¶
- Add another early return after acquisition. The successful acquisition count must still equal releases on that path; add a test rather than only inspecting stdout.
- Move acquisition after missing-input validation. Missing input should then produce zero acquisitions/releases: the contract changed, even though RAII still works.
- Inside run, record the closed count before acquiring the guard and assert it is unchanged immediately after acquisition. This passes with
_leasebut fails if the declaration becomeslet _ = Lease::new(trace): that temporary is released immediately. Restore the named binding. - Remove explicit
drop(guard)from its Rust test without adding a block. The assertion should fail because the guard is still alive there. - Explain how a C++ deleted copy constructor and a Rust move restriction each support the requirement, and where each requires additional implementation discipline.
Prefer one clear owner when the requirement is one cleanup responsibility. Add shared ownership only when independent owners really must outlive one another. Use borrowing for temporary access; use an explicit failure-reporting operation for work whose success matters to the caller. No performance winner follows from these fixture tests.
Verification and continuation¶
On October 10, 2026, the extracted examples were verified on the authorised Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust/Cargo 1.99.0 and GCC g++ 14.2.0. Rust check/formatting, four tests in debug/release, and exact outputs passed. C++20 examples passed at -O0 and -O2 with assertions enabled. Both languages' validate-before-acquire variants passed with the revised count of one; the Rust live-guard assertion passed. Moving a Rust owner and copying unique_ptr produced the intended compiler failures. The wildcard-guard and retained-guard variants compiled but failed their intended lifetime assertions. No actual resource, exceptional unwinding, process abort or performance benchmark was exercised.
Continue with borrowed access, C++ const and Rust lifetimes. Only the published entries in the course overview are available.
Previous: reading the Reference · Next: borrowing and const · Course overview