Synchronize D 2.101.0 release with tooling please
Adam D Ruppe
destructionator at gmail.com
Wed Nov 16 18:28:01 UTC 2022
On Wednesday, 16 November 2022 at 15:39:18 UTC, Dennis wrote:
> class destructors shouldn't escape `this` in the constructor,
> so a PR was made to make class destructors implicitly `scope`.
The current implementation of class dtors is broken anyway; it
doesn't even correctly implement the D spec! (The spec claims
they are "always virtual", but they are actually never virtual,
but the implementation tries to emulate it) and the statement
that "The destructor for the super class automatically gets
called when the destructor ends." is again only half-true, this
does happen when you call rtFinalize, but it is not done in
general when you call the dtor through other means.
This has profound consequences with `@safe` and other static
attributes - they just plain don't work! You can't call
`.destroy()` in a @safe (or nogc, etc.) contexts because destroy
has no way of proving the destructor actually follows those rules
at all.
If it followed the spec, you could prove it, just like with any
other virtual method, because subclasses would be forced to
follow the rules. But it would be fairly strict:
```
class A {
void dispose() @safe {}
}
class B : A {
override void dispose() @system // compile error, cannot
override safe with system
// btw since attributes are inherited when overriding, you
// don't actually have to write out `@safe`
override void dispose() @safe {
super.dispose(); // all good!
}
// but can you tighten?
override void dispose() @safe @nogc {
// signature OK, you can tighten restrictions, but...
// the spec says it must call the parent...
super.dispose(); // uh oh, @nogc this cannot call
non- at nogc super
}
}
void destroy(T)(T t) {
t.dispose();
}
A a;
destroy(a); // infers to @safe because A.dispose
B b;
destroy(b); // the best this can do is also be @safe because
B.dispose must be able to call A.dispose
// even if you made destroy call this.dtor(); then
this.super.dtor(); in a loop, like rt_finalize does today, it
could still at best infer to whatever the top-most class that
defines the dtor actually specified
```
That "problem" with the mandatory call to `super.x()`... in
effect, a destructor would not be allowed to tighten conditions.
It'd have to exactly match the parent class' interface,
attributes and all.
I use the quotes on "problem" because that's not necessarily a
problem! It'd work just fine, just you can't tighten things like
you normally do in a subclass, since the parent chain is also
required to be called.
But the current implementation we have doesn't actually treat
destructors as virtual. It just pretends they are when it is
called. The fix we need is for the compiler to treat them that
way too, and force all child classes to inherit the attributes
from the parent class.
Once you do that, you actually CAN meaningfully make a `@safe`
destructor. But until then, all destructors are necessarily
called from a `@system` context regardless; safe dtors just plain
don't work, even if you try to .destroy() it explicitly in your
`@safe` scope, because the static type system cannot prove that a
dtor in a child type, only dynamically known, didn't do something
`@system`.
So, since `@safe` dtors are currently broken anyway, what do we
gain by this change?
I'd note that typing this post took about 50x the effort than
fixing the code; this specific change is annoying but not
terribly problematic. Just I wish breaking changes came with real
world usability fixes rather than just patching one specific
scenario while leaving the rest of the design as-is. I'd rather
have to do a batched bigger fix for a bigger gain than a sequence
of small fixes.
BTW, I'd be surprised if either of those bugzilla things happened
in the real world, since it'd most likely immediately crash if
you did anyway. (Which is true of a lot of random things you do
in finalizers. They are quite tricky in what you can actually do
in them - obviously, no GC operations since it'd deadlock, it is
fairly well known you can't access GC'd members since they might
already be collected, but did you know even manually managed
members might be problematic? Since a finalizer is called from an
arbitrary thread, of the manually managed member refers at all to
a thread-local piece of data, you're back in crash city. Notably,
many Win32 gui handles are thread local, so don't try to clean
them up in a finalizier either. And there's no attribute to help
with that. Except maybe pure which is overkill lol)
> Since std.socket calls `close` in the destructor of a socket,
> `close` had to be marked `scope` as well.
This btw is also a bit iffy; I wish Socket had a method to
release its file descriptor so it wouldn't try to close it
anymore. That'd be a fairly easy additive change though, maybe
I'll PR it myself now that I'm thinking about it.
More information about the Digitalmars-d
mailing list