Rust Moves vs C++ Move Semantics¶
Rust moves transfer ownership of non-Copy values; C++ std::move changes an expression's value category so another operation can select a move overload. Neither term promises a particular machine instruction or speed improvement. Compare the source's permitted use, not just the destination's value.
Rust and C++ move requirements¶
Review ownership, Copy and Clone, closures and RAII. These independent fixtures need stable Rust edition 2024 and C++20, not GPIO or additional libraries.
Transfer a uniquely owned integer to a new owner, then reuse the original variable with a freshly owned zero. Separately, extract a pending integer and represent the now-empty slot explicitly. Compare an owning closure that only reads its captured string with a closure that returns the string to its caller. Finally, preserve both copies of a small integer.
The matched observable output is:
The output is intentionally silent about unspecified moved-from string contents, memory addresses, allocator counts and instruction counts.
C++: the cast and the selected operation¶
Save the complete program as main.cpp. unique_ptr provides a documented null source after move construction. Probe is a separate user-defined type whose counters distinguish the cast, copying and moving; its reset-to-zero policy is chosen by this example, not imposed on all C++ types.
Run both builds without defining NDEBUG, so assertions remain active:
std::move is essentially a cast to an rvalue reference type. It neither transfers a resource by itself nor strips const. A move-enabled destination may invoke a move constructor, a copyable const source may select copying, and an uncopyable const source may be rejected. A named variable declared as T&& is itself an lvalue expression; a forwarding helper needs its own deliberate category handling. Do not add std::move indiscriminately to return statements: copy-elision and return rules are separate topics.
Standard-library moved-from objects generally remain valid with an unspecified state unless otherwise specified. Valid does not mean every operation has its preconditions satisfied, nor that every container is empty. The unique_ptr constructor specifically empties its source; this example relies on that stronger contract. A user-defined type must define and preserve its own moved-from invariants. Its move operation can also fail unless its contract prevents that.
The string-returning lambda remains a callable C++ object after the first invocation. Calling it again is not prevented by a Rust-style FnOnce contract; what it returns depends on the captured string's moved-from state. Our example calls it once and makes no assertion about that state. If one-shot execution is an application requirement, model availability explicitly instead of treating std::move as a one-call restriction.
Rust: ownership transfer without a move constructor¶
Run cargo new moves --edition 2024 and replace src/main.rs with this complete source:
Inside that project:
The Rust move rules leave the source place deinitialised until reassignment. This is not an empty Box that can still be inspected, and Rust does not invoke a user-defined move constructor. A move may be implemented with relocation or eliminated entirely; ownership rules do not specify its runtime cost. Drop concerns eventual destruction, not a callback whenever a value moves.
Copy types such as this integer preserve the source in a by-value use. Clone is an explicit operation with type-specific semantics; cloning a reference-counted owner shares an allocation, whereas this String clone keeps independent string data. Neither copying nor cloning universally means the same cost or ownership structure.
The closure call traits depend on what the body does with captures. A move closure takes captures by value but can still implement Fn if it only reads them. Returning the captured non-Copy String consumes it, so that closure can only be called by value once. Owning a captured reference does not extend its referent's lifetime or automatically satisfy a thread's static lifetime requirement.
Changed requirement: extract through a borrowed owner¶
If a caller retains a mutable reference to an owner, leave a valid replacement before returning the old value. Our optional slot uses take to leave None and can later be refilled. A non-optional slot can use mem::replace to install a replacement, or mem::take if a suitable Default exists. Choosing None versus a default value is part of the API contract: zero is not an empty reading.
C++ exchange makes replacement explicit too. Merely moving from a field leaves whatever state that type's move operation defines. For an optional
Partial Rust moves are possible for eligible fields, but leave the whole aggregate unavailable while a field is uninitialised. Moving a non-Copy field out of a type implementing Drop is restricted because its destructor expects its value to remain intact. Use an explicit extraction method that maintains the invariant, rather than adding unsafe code to bypass the restriction.
Intentional compiler failures and repairs¶
Compile this Rust program independently. A mutable borrow grants access, not permission to return the owner's value while leaving the caller's slot uninitialised:
Repair the body with std::mem::replace(owner, Box::new(0)) if zero is the documented replacement, then assert that the returned value is 46700 and the caller owns zero. If emptiness is meaningful, change the interface to the optional slot in the working example. Borrowing only to inspect the integer is another legitimate design; it is not ownership transfer.
Compile this C++ program separately. std::move does not remove const and unique_ptr cannot be copied:
If transfer is required, make the source non-const before moving and check the documented null state. If only read access is required, retain the owner and borrow instead. Do not cast away const to manufacture an ownership-transfer API.
Selection criteria and exercises¶
| Requirement | Rust candidate | C++ candidate |
|---|---|---|
| Inspect without acquiring ownership | Shared borrow | const reference / documented non-owning view |
| Transfer a unique owner | By-value non-Copy parameter or assignment | Move-enabled owning type plus appropriate expression |
| Retain two independent string values | Explicit String clone | String copy |
| Extract and leave explicit absence | Option::take | Defined extraction protocol, such as exchange to null |
| Call an owning read-only closure repeatedly | move capture whose body supports Fn | Owning capture with read-only body |
| Enforce one-shot consumption | FnOnce boundary | Explicit state or separately designed API |
- Add an empty-slot test and refill it with zero. Distinguish absence from zero on the second extraction.
- Remove the Rust reinitialisation and read source after moving it. Expect rejection, not a null pointer. Then repair by borrowing or assigning a new owner, according to the requirement.
- Call the consuming Rust closure twice; it must fail. Keep the read-only move closure callable twice and explain why the keyword alone does not select FnOnce-only behaviour.
- Add a Drop implementation to a struct with a String field, then attempt to move that field out. Repair with replacement while preserving the destructor's contract.
- Explain the Probe const-copy counter and why moved-from unique_ptr nullness says nothing about moved-from string emptiness. No benchmark follows from these tests.
Verification and next step¶
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, four debug/release tests and exact outputs passed. Replacement and Drop-preserving repairs passed their assertions. Separate borrowed extraction, moved-source reading, repeated consuming closure and Drop-field extraction were rejected with E0507, E0382 and E0509. C++20 output, Probe counters, non-const transfer repair, empty-slot extraction and zero refill assertions passed at -O0/-O2; moving the const unique_ptr was rejected. Sources/logs were retained and generated Cargo targets cleaned. These checks establish selected contracts, not move costs or a formal equivalence between languages.
Next: address stability and Pin, including the difference between moving an owner and moving the value it points to.
Previous: error contracts · Next: address stability and Pin · Course overview