Is it possible to deinitialize the class without calling the gc?
Adam D. Ruppe
destructionator at gmail.com
Sat Aug 4 04:51:55 UTC 2018
On Saturday, 4 August 2018 at 02:21:48 UTC, 12345swordy wrote:
> Which is converted to void type when passing the object to
> rt_finalize, which causes to lost all type information.
It still has run-time type information, but indeed, the compile
time info is lost.
>> Child classes have independent destructors which do not need
>> to adhere to the attributes of the parent class destructor,
> Why is this an issue? Simply don't call them.
Then the object isn't fully destroyed....
scope BaseClass c = new DerivedClass();
if that only destroys BaseClass' members, you have some serious
leakage on whatever DerivedClass happened to add to it.
Remember, that code is always legal, and the actual type of c may
not be known until runtime:
BaseClass c = (readln() == "lol\n") ? new DerivedClass : new
OtherClass; // legal code!
Or, of course, destruction of the base is important too:
scope DerivedClass c = new DerivedClass();
if that leaves BaseClass' members hanging, again, serious leakage.
You could redefine the language so each child class's destructor
is also responsible for cleaning up the base class explicitly...
but that'd be really ugly to write reliable OO code with since it
wouldn't actually inherit cleanup behavior. Really, the child
classes need to call the base class destructor, if not
implicitly, it needs to be required explicitly somehow.
> How is this a better solution!?
It is the ONLY solution right now. If you're writing a DIP, you
might be able to change that, but first you need to understand
the current situation and the reasoning behind it.
If you are looking for a @nogc destroy function, the language
MUST change, and this isn't as simple as many people think it is.
Specifically, destructors will have to start acting like other
virtual methods, with mandatory attribute inheritance or
tightening, but never loosening.. yet also keeping the hidden
calls to the super function.
Let me show a few examples.
---
class A {
@nogc ~this() {}
}
class B : A {
~this() {} // MUST become an error: non- at nogc destructor
cannot "override" @nogc parent destructor
}
---
This part is the same as ordinary method inheritance in D (but
see below).
Once that's in place, it would become possible for .destroy to
take it argument by template and act on the @nogc compile time
attribute. But note:
---
A a = new B();
destroy(a); // would NOT be @nogc since the knowledge that it is
a B is lost here, due to separate compilation requirements
---
And, moreover, if the child destructor is non-trivial, adding to
it even with the strengthening restriction is impossible - I put
scare quotes around "override" above because it doesn't actually
override, it stacks the functions.
---
class A {
~this() { /* non-trivial destructor present */ }
}
class B {
@nogc ~this() {} // Error: @nogc B.~this must call A.~this to
clean up, but can not because A.~this is not @nogc
}
---
This is *different* than ordinary method inheritance in D.
Normally, the child class method is allowed to override and
tighten the interface. But with destructors, it MUST call super
to avoid resource leaks (and in D, does so implicitly), so it is
also sticking to the wider interface.
Now, here's where it gets really crazy. A class' destructor can
be non-trivial if ANY of its members have a non-trivial
destructor:
---
struct S {
~this() {}
}
class A {
S s;
}
class B : A {
@nogc ~this() {} // guess what? Error: @nogc B.~this cannot
call A.~this because it calls non- at nogc function S.~this.
}
---
Both the call to A.~this and the very existence of A.~this and
the fact it calls S.~this are implicit... but both are important
to ensure that struct is properly cleaned up when the time comes.
Now, what about this?
---
class A {} // note that there is no destructor
@nogc void main() {
A a = getSomeA();
destroy(a); // is this legal?
}
---
The only correct answer is: no, that is not legal. A itself has
no destructor, so calling it BY ITSELF would be @nogc compatible.
But, you have no idea what child class you actually have there.
`getSomeA()` might have returned:
---
class B : A {
~this() { /* lol ima call the GC */ }
}
---
Since A has no destructor, that child is legal; A did not
prohibit the GC call. (And if it did, that would be fairly
annoying, though it is a valid argument to make - to say that ALL
destructors must be @nogc, especially since the status quo
includes InvalidMemoryOperationError. But... it hits the
false-positive pain of @nogc, and can break currently-valid code
that calls the GC in a correct manner to avoid the invalid memory
operation errors.)
But if that child is legal, the destroy function MUST assume it
might get one of those, and thus make no compile-time guarantees.
So, even with the changes I outlined above, @nogc destroy would
be valid if and only if it is passed a class whose *static* type
explicitly includes the @nogc guarantee, which is only allowed if
all the parent chains, all the way up, have either trivial
destructors or their own @nogc guarantees.
> Do I need to add meta information section to my DIP?
We already know the information at run time. But not at compile
time since subclasses may be compiled totally separately to base
classes. (They may even reside in totally separate DLLs and
change based on runtime decisions, like what plugins the user
actually has installed on their computer, or like my little
example above, you could make it depend on user input.)
But you can derive the bits you need from compile time info if
the child destructors were restricted as described above. Unless
I'm missing something too....
Just there's a bunch of cases here that the current language has
no solution for.
of course you could just do the sane thing and pretend @nogc
doesn't exist. then this stuff becomes a lot easier and you can
just get back to work.
More information about the Digitalmars-d
mailing list