> in Rust it's a compile-time error to use a variable whose value is moved out, whereas in C++ the variable is still usable but has an invalid value
C++ does it this way because there are common cases in systems code where doing it the Rust way would literally be unsafe. Not all memory references are visible at compile-time and may exist outside the address space.
Would you mind elaborating more on those common cases? I'm not sure I've heard of destructive moves being less safe than non-destructive moves and I'm not smart enough to figure out what you're talking about in your second sentence.
Shared address space. Some other process or silicon can read or write the object you just moved but doesn’t know you moved it. You need to keep the memory previously occupied by the moved object valid long enough for those references to realize you moved it to prevent corruption.
A typical case is high-performance I/O, which uses a lot of DMA. DMA is oblivious to most programming language semantics like lifetimes, ownership, etc and will happily step all over your address space if you aren’t careful.
I'm curious to hear more about this use case. The DMA I do in rust is generally static buffers, because I'm not sure how to pass the borrow checker otherwise. (There are ways). Generally, you set up a static [u8] buffer, and pass its pointer to the hardware that's doing the DMAing. Then magic, then the buffer gets read or written by the hardware. In this scenario, the variables never go out of scope. Am I cheating, and avoiding this issue by using static buffers? If the buffer drops during a DMA transfer, I believe UB happens.
I'm suspicious a similar principle happens with memory-mapped flash memory as well, e.g. QSPI.
This just means that affine types aren't the right tool to model memory that you don't have full control over. Which is true, but also represents a very small subset of overall data. Rust provides you with other tools to handle those kinds of situations.
There is a small wart here, which is that (with async Rust) some of these use cases would benefit tremendously from full-fledged linear types, or at least an easy way to run code during async cancellation.
The difference between an affine and a linear type is that the ways in which a linear type is consumed are controllable through encapsulation — for example, imagine you have a type which represents a certain amount of money, and you want to statically prevent the money from being dropped on the floor. Affine types don't prevent that statically, but linear types do. You can still have runtime checks though.
> Some other process or silicon can read or write the object you just moved but doesn’t know you moved it.
That should primarily affect buffers that are inline with the moved object, right? i.e., not static buffers or stuff that's heap-allocated? How common is that scenario? I admittedly generally thought DMA used static buffers, though to be fair I'm not exactly highly experienced in the space.
> You need to keep the memory previously occupied by the moved object valid long enough for those references to realize you moved it to prevent corruption.
How is this (reliably) handled in C++? I feel there's gotta be more than just hoping the empty object hangs out long enough for the rest of the system to catch on (e.g., moving things around near the end of a scope when the empty object will be destroyed "soon").
C++ does it this way because there are common cases in systems code where doing it the Rust way would literally be unsafe. Not all memory references are visible at compile-time and may exist outside the address space.