On Wednesday, 28 May 2025 at 06:06:25 UTC, Dom DiSc wrote:
>This is because for some objects it may be very expensive or even not possible to create a temporary copy. This is avoided by the xor-construct.
Okay, but that's a separate problem. I'm just going to discuss the @safe
interface of swap for simple pointer types, because if we throw performance and special type considerations in the mix it only gets more confusing.
But why is this limitation necessary? What can possibly go wrong?
Memory corruption in @safe code.
import std.stdio, std.algorithm;
void main() @safe
{
string x = "hello";
void replace(ref string x)
{
immutable(char)[4] buf = "bye";
string y = buf[]; // y contains a reference to local variable `buf`
swap(y, x); // y is assigned to x which has a longer lifetime than `buf`
}
writeln(x); // hello
replace(x);
writeln(x); // @�, (`buf` is freed, printing garbage memory)
}
Let me clarify a few things.
Ignoring bugs, D without dip1000 / scope
pointers is already memory safe if you stick to the @safe subset. You can freely swap the values of any combination of local and global variables without problem. It's all @safe because pointers always point to global memory or GC managed heap memory, preventing use-after-free scenarios.
Now there is one scenario where memory gets freed but not because the GC or the end of the program triggered it: local variables. At the end of a function, the stack frame gets cleaned up, freeing all memory used for local variables in that function. Since you can only refer to a local variable name inside the function, direct access is always safe. A problem arises however when you keep a reference to the local variable alive either by returning a nested function, or creating a pointer to the variable.
To solve the case with nested functions, the compiler creates a closure with the GC so the stack gets promoted to heap memory. For pointers however, the compiler doesn't do that. It could, but stack pointers are usually created specifically for performance reasons. If you want GC memory you might as well use new int[]
or an array literal instead of slicing a static array.
This does mean that suddenly we're dealing with pointers that can't freely be assigned to anything anymore, but only to things that remain in the same scope as the variable you took the address of. So all this talk about ref-counting, borrow-checking, scope
pointers and whatnot is about allowing a new scenario where you have a variable that frees itself when it goes out of scope, but you want to allow creating a pointer or 'reference' to that variable which may not outlive the variable is was created from.
Again, D is already safe if you simply don't allow creating such references to objects that destruct themselves. You can already do safe manual memory management by allocating in the constructor, implementing a copy constructor, disabling field access, and freeing in the constructor, but that severely restricts use of the allocated payload.
For example, you can have a @safe reference counted string, but you can't pass the underlying char[]
to writeln
, you have to index every char
manually and pass that. Why? Because writeln
might assign the char[]
to a global or something else that lives longer than the reference counted string.
Yes, but why does the compiler need to know that the two have been swapped?
It doesn't for regular pointers or non-pointers. swap(ref T x, ref T y)
is always safe when T = int, but when T = string and x
has a pointer to char
bytes that are stack-allocated/reference counted/other non-gc pointer, and y
is a variable that is still accessible after those char
bytes have been destructed, then you get a dangling pointer, which is obviously not @safe
.