Rust Borrowing vs C++ const: References and Lifetimes¶
Rust shared references and C++ const references both restrict operations through an access path, but they are not the same contract. Compare who may still change the object, which state may change through a read-like method, and who keeps borrowed storage alive before translating a C++ API into Rust.
Requirement: inspect a reading without copying its owner¶
Continue after values, ownership and RAII, with the borrowing and lifetime lessons available for review. Use Rust edition 2024 and a C++20 compiler; neither program reads hardware or uses threads.
An owner stores a fixture reading. Inspection returns a copied scalar and increments a diagnostic counter, but must not change the reading. Updating the reading is a separate operation. A label can be viewed without duplicating its text, provided its owner stays valid. These are deliberately single-threaded requirements, not a sensor-driver interface.
C++ const describes an access path¶
Save this complete program as main.cpp:
Run both assertion-enabled builds:
A const access path does not turn an originally non-const object into a permanently frozen object. Updating sensor through its non-const owner and then using view is allowed here. A genuinely const object cannot have its ordinary reading member modified; mutable permits the diagnostic field to change even in fixed. Neither property is thread synchronisation. See the C++ working draft's cv qualifiers; the example uses established C++20 mechanisms, not newer draft-only features.
The string_view contract describes a view of character storage, not ownership of that storage. The returned label remains valid here because owner lives and is not changed before the view is used. Its signature does not encode this caller obligation as a Rust lifetime relationship. Do not keep a view across owner destruction or a mutation that invalidates its storage.
Rust separates ordinary borrowing from interior mutation¶
Create a package:
Replace src/main.rs with this complete program:
Run these checks with the exact source:
Four tests should pass. Both working programs produce:
Equal output does not mean equal permitted access patterns. Rust ends the shared borrow of plain before assignment because observed is no longer used; the C++ program uses its const reference after assignment. Moving the Rust assertion after assignment changes whether the program is accepted.
Why &T is not simply const T&¶
Rust reference types distinguish shared and mutable references. While an ordinary shared borrow must remain usable, you cannot take conflicting exclusive access to its data in safe Rust. Mutable references provide exclusive access subject to reborrowing and the operations the borrow checker permits; C++ non-const references do not impose that same general exclusion rule.
The interior-mutability rules allow designated state to change through a shared reference. Here, Cell holds a copied diagnostic integer; no reference to its inner integer escapes. Cell is not Sync, so this type is not automatically suitable for shared cross-thread inspection. C++ mutable also does not make simultaneous increments safe. The counters use small bounded fixtures; a production counter additionally needs an overflow policy.
| Concern | C++ working example | Rust working example |
|---|---|---|
| Read-like method | const member function | &self receiver |
| Diagnostic change | mutable member | Cell inside the value |
| Reading update | Non-const access path | &mut self receiver |
| Borrowed text | string_view with caller lifetime obligations | &str tied to the input borrow |
| Thread sharing | Requires a separate data-race/synchronisation contract | Cell prevents Sync; choose a suitable design if sharing is needed |
These are comparisons of specific APIs, not substitutes for their full language rules. Neither const nor a lifetime annotation is a physical measurement or an application correctness proof.
Reject a conflicting access, not just an unwanted output¶
Save the normal Rust source and temporarily replace it with:
cargo check rejects the assignment with E0506 on the verified compiler, because the borrow is used afterward. Repair it by using observed before assigning reading, then take a new borrow if needed. This is not a demand for an unnecessary clone.
Compile this separate invalid C++ source as failure.cpp:
It fails because assignment through this const reference is forbidden. Assignment through reading is a different access path and is allowed for this non-const object. Do not use const_cast to mutate a genuinely const object: a cast does not make that operation well-defined.
Change the requirement: keep a label after the owner changes¶
Borrowing is suitable when the caller can preserve the owner and its storage. If a report must keep the old label after its source is replaced or destroyed, give the report an owned String/std::string instead of a borrowed view. This is an explicit snapshot requirement, not “clone whenever the compiler complains.”
Rust's label signature uses lifetime elision: the one input-reference lifetime is assigned to the output reference. Writing the same relationship explicitly does not prolong the owner's life. A function cannot return a borrowed view of its local String. A C++ string_view of local character storage likewise does not become owning because the function returns it, although such a program may compile. Do not execute a dangling-view demonstration to investigate it.
For a snapshot, copy while the source is valid, mutate or destroy the source, then test that the report retains its original label. Prefer a borrow for immediate inspection and an owned value for independently retained data. Shared ownership introduces another lifetime policy and should not be the default replacement for either.
Exercises and verification¶
- Move the Rust observed assertion after assignment. It must fail compilation; then use the value before mutation and verify the repaired program.
- Try returning a view of a local String. Rust must reject the returned borrow; do not run the analogous invalid C++ view.
- Borrow owner as &str, change owner, and use the old view afterward. Rust must reject the conflicting borrow. Obtain an owned snapshot before mutation to satisfy the changed requirement.
- Remove Cell/counter changes and the C++ mutable counter when diagnostics are unnecessary. A completely read-only inspection contract is simpler; update the tests to match rather than pretending the old contract remains.
- Explain why a shared reference permitting diagnostic changes is neither an exclusive borrow nor a thread-safe counter.
On October 10, 2026, the authorised Raspberry Pi 4B verified the extracted programs 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 debug/release tests and exact outputs passed. C++20 passed at -O0 and -O2 with assertions enabled. The borrow-before-mutation repair and owned snapshots passed; the C++ snapshot retained its label after clearing the owner in both builds. Rust rejected live-borrow assignment, a returned local borrow and owner mutation with E0506, E0515 and E0502. C++ rejected assignment through the const reference. No undefined behaviour, concurrent counter access or hardware input was executed.
Continue with traits, inheritance, generics and dispatch. Follow only the published entries in the course overview.
Previous: values and RAII · Next: traits and dispatch · Course overview