Rust Pin and C++ Address Stability¶
Rust Pin and C++ stable-address designs concern where a value remains, not merely who owns it. Moving a heap owner can leave its pointee in place, but an ordinary pointer wrapper does not necessarily prevent extraction or replacement. Address stability also does not keep an expired object alive.
Rust and C++ address requirements¶
Read Rust moves vs C++ move semantics and Pin and Unpin first. Use Rust edition 2024 and C++20; the independent fixtures do not access hardware.
Keep an integer-bearing record at the same address while transferring its heap owner into a growable owner collection. Force the collection to reserve beyond its current capacity, then verify the record's identity and reading. Permit a controlled change of the reading to zero without moving the record. Contrast this restricted record with an ordinary owned string that can be extracted.
These fixtures do not implement a self-referential record, an intrusive list or an async runtime. They expose the access restrictions needed to discuss those designs without executing dangling-pointer examples. Expected output in both languages:
C++: allocate the record, move its owner¶
Save this complete program as main.cpp. Sample explicitly deletes copy and move operations. Its unique_ptr remains movable, so a vector can relocate the owning pointers rather than the records.
Compile and run with assertions enabled:
vector reserve invalidates references to vector elements on reallocation. Here those elements are owning pointers; the separately allocated records are not vector elements. Never keep a reference to the old unique_ptr element across that operation. The unique_ptr move constructor transfers the pointer and empties the source.
Deleting Sample's move operations blocks ordinary direct movement of that type. It does not provide a universal pinning/lifetime protocol for observers, nor prevent an owner from resetting or destroying it. A raw observer must stop using the record before destruction. C++ permits explicit lifetime/storage operations too; the class's normal API restrictions are not a proof against every possible misuse.
Rust: Pin protects the pointee, not the owner¶
Run cargo new address_contract --edition 2024 and replace src/main.rs with this complete program:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |
In that project:
PhantomPinned prevents automatic Unpin implementation. It does not pin a newly constructed value by itself, and the last test deliberately moves that unpinned value. This differs from C++ deleted move constructors: Rust has no corresponding constructor hook or general non-movable-by-value type declaration.
The Pin API restricts access to a !Unpin pointee; the owning Pin
The pinning contract also concerns invalidation and destruction, not just an observed address. Pin does not extend an observer's lifetime, perform synchronisation or make arbitrary unsafe projection sound. This fixture uses no unsafe block or self-pointer; PhantomPinned is intentionally added to demonstrate API restrictions, not because this integer record requires pinning for correctness.
Changed requirement: observers survive collection changes¶
If observers borrow records temporarily, retain owners for the whole borrow. Rust can keep a shared reference to a pinned record for inspection, but that borrow may prevent a conflicting owner-collection mutation; address stability alone does not remove borrowing restrictions. Separate the storage owner from the operation requiring mutation, or use an explicit handle lookup when that better matches the requirements.
If observers must outlive removal, a stable address is insufficient: the record may be gone. Decide whether observers should retain shared ownership, hold a Weak reference that can fail to upgrade, or use an ID that can report removal. Never turn a pointer's numeric address into a permanent identity without a lifetime/reuse policy. Later design lessons compare those choices with ECS.
If the record contains its own references, a plain Box is not the whole solution: its value may still be extracted. In Rust, constructing such an abstraction involves additional invariants, projection and drop obligations. Follow the standard pinning documentation before implementing unsafe internals; this article intentionally does not offer a hand-written self-referential implementation.
Intentional failures: extracting the protected value¶
Compile this Rust program separately. get_mut requires the pointee to implement Unpin, so this attempt is rejected:
Repair inspection with owner.as_ref().get_ref(), or expose a narrow pinned receiver method as above. Do not add an Unpin implementation merely to silence this error when an address-sensitive invariant requires pinning. If the type truly has no such invariant, redesigning it to be Unpin is a different legitimate decision.
This C++ direct move is separately rejected because the class explicitly deletes its move constructor:
Repair owner transfer by allocating Sample and moving its unique_ptr, not moving *owner. A stable-address owner design still needs a policy for destruction and observer expiry. Neither failure proves a full equivalence between the language mechanisms.
Part 5 checkpoint: defend the contracts¶
Use the six independent comparison fixtures, rather than constructing one application that hides their differences. For each answer, name the owner, permitted observations, expected failure and the test that distinguishes the alternatives.
- Values and cleanup: move validation before acquisition. Which cleanup counts change? Explain why scope cleanup cannot guarantee a successful external commit.
- Borrowing and const: retain a label after its original owner changes. Choose a borrow or an owned snapshot, and explain why const is not a lifetime guarantee.
- Dispatch: add a differently shaped source. Contrast an open behaviour interface with a closed enum; justify ownership separately from dispatch.
- Error contracts: change best-effort reporting to stop-first. Preserve Format/Range and distinguish an error return, exception unwinding and a panic.
- Moves: extract a pending zero, then extract again. Preserve explicit absence and explain why Rust deinitialisation is not a C++ moved-from object's empty state.
- Address stability: grow the owner collection, update a record and then remove it. State which observations remain valid at each boundary. Does pinning justify keeping a raw pointer after removal?
Expected distinctions: validation-first skips acquisition for invalid input; snapshots own retained data; dispatch does not dictate allocation; stop-first attempts fewer inputs; empty slots are not zero; removing the last owner ends the record's lifetime even if its address never changed. A justified alternative is acceptable if it meets the same explicit requirements.
Selection criteria and verification¶
| Need | Candidate | Remaining obligation |
|---|---|---|
| Ordinary values with no address dependence | Owned values and normal moves | Define ownership and failure behaviour |
| Stable allocation while owner collections change | Separately allocated records | Keep owners alive and respect collection-element invalidation |
| Address-sensitive Rust abstraction | Pin-based API for !Unpin data | Uphold projection, invalidation and destruction contracts |
| Observers that may see removed records | Handles/Weak/shared ownership as appropriate | Define expiry, lookup failure and identity reuse |
On October 10, 2026, the extracted sources passed 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, exact-source formatting, five debug/release tests and exact outputs passed. The shared-inspection repair compiled; unrestricted mutable access and into_inner for the !Unpin record were rejected with E0277, and dropping a borrowed owner was rejected with E0505. C++20 output and the owner-transfer/growth/removal repair assertions passed at -O0/-O2; direct record movement was rejected. Sources/logs were retained and generated Cargo targets cleaned. Address identity assertions are not performance, allocator-efficiency or general soundness proofs.
Next: choosing functions, generics or trait objects for the variation handled by Strategy.
Previous: Rust and C++ moves · Next: Strategy · Course overview