This is a fair point, but I ultimately agree with the jist of @fclout's argument:
I would go even further and say that inout
and borrow
usage of an unsafe pointer also have to be considered unsafe.
We can distinguish between two possible meanings for the keyword unsafe
:
- A dictionary-like definition: Memory corruption might happen here.
According to the dictionary-like definition, @Joe_Groff is right: Memory corruption can't happen here. It might happen elsewhere, but not here.
This option is the most parsimonious: Nothing about unsafe pointer needs to be unsafe -- only dereference.
FWIW, this appears to be the definition Rust uses, at least for raw pointers: "We can create raw pointers in safe code; we just canât dereference raw pointers outside an unsafe block..."
I don't think we should copy Rust in this case, but at least that's an indication that some folks find that kind of programming workable.
- A process-oriented definition: The programmer needs to verify an outside-the-language invariant here. Once the programmer verifies this invariant locally within the unsafe expression (assuming they've done so correctly), the rest of the program is memory safe globally (until the next unsafe expression -- rinse, repeat).
It's the process-oriented definition that requires unsafe
here. We seek a process that invites the programmer and/or reviewer to read the local text of the program, in combination with surrounding documentation, and thereby verify an outside-the-language invariant.
The specific invariant we need to verify when we copy a pointer is shared ownership / lifetime: Do the copied-from pointer and the copied-to pointer agree on how they are going to share ownership of the pointee?
(And in the case of borrowing
and inout
, the invariant we need to verify is that the caller will keep the pointee alive for the duration of the call.)
This process-oriented definition has been working really well for us in the application of bounds and type safety to C++. We treat pointer arithmetic and pointer casting as an error at the point of arithmetic / cast -- not because that's the location where the memory corruption happens, but rather because that's the location where you have a chance to verify the invariant (with a runtime bounds check or type check).
In the alternative, the programmer has no hope: Here's a pointer. It has previously undergone an unknowable set of transfers between owners, increments and decrements, and casts. Now, is it still alive, correctly typed, and in bounds? Oy!
You could argue that programmers should know not to pass pointers around willy-nilly. But what will make them know? The unsafe
keyword is how we, as language designers, share the know{ledge}.
This is a slightly different concern. It's 100% true that a pointer used in this sense is perfectly safe. The reason we need to consider it unsafe is a limitation of our type system. Our type system -- both UnsafePointer
, and mmap's void*
-- does not express "a pointer that I will never dereference", so we are forced to treat this as a pointer that I will dereference. You could imagine expanding the type system to represent "a pointer that I will never dereference", but I don't think the juice is worth the squeeze.