On Monday, 12 May 2025 at 19:28:10 UTC, Walter Bright wrote:
>This comes about because old
, being declared after ctx
, has a shorter lifetime than ctx
. If old
has a destructor, that destructor will run before ctx
s destructor, and ctx
will be referring to an invalid instance of old
.
You can't free a pointer that has aliases in @safe
code. Therefore you can't simply have a destructor that performs a free(), the struct that has it needs to prevent aliasing by disabling copies with @disable this(this)
, or by providing a copy constructor that duplicates the pointer. Otherwise you allow memory corruption:
import std.stdio;
@safe:
struct S
{
int* x;
~this() scope @trusted { writefln("~this(): free(%s)", cast(void*) x); }
}
void f(ref scope S ctx)
{
{
auto old = ctx;
}
writefln("use %s", cast(void*) ctx.x);
}
void main()
{
scope S s = S(new int(38));
f(s);
}
This prints:
~this(): free(7F8D32557000)
use 7F8D32557000
~this(): free(7F8D32557000)
Both a 'use after free' and a 'double free'! But if you add this(this) scope { x = new int(*x); }
to S, it works out.
If S does not have a destructor, does that make the error a false positive or not? I decided that the existence of a destructor should not change the scoping rules.
I agree with that, but in light of the above, I don't think a copy of a scope pointer should have its lifetime shortened to the destination variable.
It may be by design, but it's a false positive in the sense that it leads to unnecessary refactoring (like https://github.com/dlang/phobos/pull/8125), and to unnecessary @trusted
code. The ctx = old
example I gave is reduced from my actual code:
https://github.com/dkorpel/ctod/blob/1651fb4e31095e0cf5e0c4524149bdc2bcc7539e/source/ctod/cdeclaration.d#L231
If you have an alternate way to write that in a @safe
way, I'd love to hear it.
BTW, thank you for your post. It's informative and excellent.
:)