Jump to page: 1 2 3
Thread overview
Don't expect class destructors to be called at all by the GC
Dec 21
user1234
Dec 22
Mengu
Jan 31
Bienlein
Jan 31
DanielG
Jan 31
DanielG
Dec 22
bauss
December 21
"Don't expect class destructors to be called at all by the GC"

I was a bit shocked to read that here: https://p0nce.github.io/d-idioms/#The-trouble-with-class-destructors

The document tries to clarify with:
"The garbage collector is not guaranteed to run the destructors for all unreferenced objects."

Unfortunately, that doesn't really shed much light on this oddity.  So, specifically, under what circumstances are destructors not called?

Thanks,
Mike
December 21
On Thursday, 21 December 2017 at 02:57:00 UTC, Mike Franklin wrote:
> "Don't expect class destructors to be called at all by the GC"
>
> I was a bit shocked to read that here: https://p0nce.github.io/d-idioms/#The-trouble-with-class-destructors
>
> The document tries to clarify with:
> "The garbage collector is not guaranteed to run the destructors for all unreferenced objects."
>
> Unfortunately, that doesn't really shed much light on this oddity.  So, specifically, under what circumstances are destructors not called?
>
> Thanks,
> Mike

When the GC is unaware of a class instance (an "unreferenced object") it won't call its destructor. This happens when you use alternative memory management strategy, for example using Mallocator and make you get an unreferenced object that you have to manage yourself.
December 21
On Thursday, 21 December 2017 at 04:10:56 UTC, user1234 wrote:
> On Thursday, 21 December 2017 at 02:57:00 UTC, Mike Franklin

>> Unfortunately, that doesn't really shed much light on this oddity.  So, specifically, under what circumstances are destructors not called?
>>

>
> When the GC is unaware of a class instance (an "unreferenced object") it won't call its destructor. This happens when you use alternative memory management strategy, for example using Mallocator and make you get an unreferenced object that you have to manage yourself.

The root of the problem is that in D, class destruction and finalization are conflated. It would be much more accurate to refer to ~this in classes as a finalizer. Then this sort of confusion wouldn't be so widespread.

Also, consider the current GC implementation finalizes any objects that haven't yet been finalized when it terminates. It terminates during runtime termination, but *after* static destructors are executed (which is how it should be). We already know that you can't rely on any GC memory references to be valid in a class destructor, but because of this cleanup phase, you also can't rely on any program state still being valid.

As an example, each of the Derelict packages used to (pointlessly) unload its shared library in a static destructor, but people repeatedly had segfaults at app exit because their class destructors were calling into the loaded libraries to release resources. The solution there was easy -- stop manually unloading the libraries and let the OS do it at process termination. But anyone who wants to unload any resources in a class destructor needs to be aware of this issue in case a static destructor somewhere is getting to it first. And I still see code using Derelict where people unload the library themselves in a static destructor.

I just don't even bother with class destructors. Without a guarantee that they can run and without any sort of deterministic behavior, it's really not appropriate to refer to them as destructors and they're about as useful as Java finalizers, which means not at all. In order to make them less error prone, we need to separate the concept of destruction from finalization and allow both destructors and finalizers. That's what I've taken to doing manually, by implementing a `terminate` function in my classes that I either call directly or via a ref-counted templated struct called Terminator.
December 21
On Thursday, 21 December 2017 at 06:50:44 UTC, Mike Parker wrote:
> That's what I've taken to doing manually, by implementing a `terminate` function in my classes that I either call directly or via a ref-counted templated struct called Terminator.

I feel like I'm rambling but..
The problem with that approach is that you can't reuse Unique, RefCounted, scoped!T because they rely on .destroy
December 21
On Thursday, 21 December 2017 at 14:26:55 UTC, Guillaume Piolat wrote:
> On Thursday, 21 December 2017 at 06:50:44 UTC, Mike Parker wrote:
>> That's what I've taken to doing manually, by implementing a `terminate` function in my classes that I either call directly or via a ref-counted templated struct called Terminator.
>
> I feel like I'm rambling but..
> The problem with that approach is that you can't reuse Unique, RefCounted, scoped!T because they rely on .destroy

I'm not proposing it as a general solution. It's easy to implement and it works for my use case, so it's one possible solution.
December 21
On Thursday, 21 December 2017 at 06:50:44 UTC, Mike Parker wrote:
> On Thursday, 21 December 2017 at 04:10:56 UTC, user1234 wrote:
>> [...]
>
>>>[...]
>
>> [...]
>
> The root of the problem is that in D, class destruction and finalization are conflated. It would be much more accurate to refer to ~this in classes as a finalizer. Then this sort of confusion wouldn't be so widespread.
>
> [...]

Have you considered writing a DIP on this?
December 21
On Thu, Dec 21, 2017 at 06:50:44AM +0000, Mike Parker via Digitalmars-d-learn wrote: [...]
> I just don't even bother with class destructors. Without a guarantee that they can run and without any sort of deterministic behavior, it's really not appropriate to refer to them as destructors and they're about as useful as Java finalizers, which means not at all. In order to make them less error prone, we need to separate the concept of destruction from finalization and allow both destructors and finalizers. That's what I've taken to doing manually, by implementing a `terminate` function in my classes that I either call directly or via a ref-counted templated struct called Terminator.

I recently ran into this problem while using Adam Ruppe's lightweight SQLite binding (arsd/sqlite.d). Originally, I kept an open database handle (which is a class instance) throughout the lifetime of the program; in this case, I could just use a scoped reference and it would ensure the DB is closed when the handle went out of scope, just what I want.  But as my code developed, I began to need to cache multiple DB handles for performance, and scope no longer helps me there. At first I thought, no problem, the GC would handle this for me. Right?

Wrong. Even calling GC.collect directly did not guarantee the DB handle was closed at the right time.  This may have been a bug in my code that left dangling references to it, or perhaps the array of Database handles was still scanned through by the GC even though the only remaining array slice has a shorter length. Whatever the reason was, it left me with the very unpleasant prospect of silently accumulating file descriptor leaks.

I ended up calling .destroy on the class instance explicitly just so the destructor would run at the right time, right before nulling the reference so that the GC would collect the memory.

This makes using classes in D an even dimmer prospect than it already generally is (nowadays, and I don't seem to be the only one, I prefer to just use structs and templates instead of runtime polymorphism, where possible).  When the scoped destruction of structs isn't an option, RefCounted!T seems to be a less evil alternative than an unreliable class dtor. :-/


T

-- 
MACINTOSH: Most Applications Crash, If Not, The Operating System Hangs
December 21
On Thursday, 21 December 2017 at 18:20:19 UTC, H. S. Teoh wrote:
> When the scoped destruction of structs isn't an option, RefCounted!T seems to be a less evil alternative than an unreliable class dtor. :-/

Alas, RefCounted doesn't work well with inheritance...

Though, what you could do is make the refcounted owners and borrow the actual reference later.
December 21
On Thu, Dec 21, 2017 at 06:45:27PM +0000, Adam D. Ruppe via Digitalmars-d-learn wrote:
> On Thursday, 21 December 2017 at 18:20:19 UTC, H. S. Teoh wrote:
> > When the scoped destruction of structs isn't an option, RefCounted!T seems to be a less evil alternative than an unreliable class dtor. :-/
> 
> Alas, RefCounted doesn't work well with inheritance...

Oh?  What's the issue?


> Though, what you could do is make the refcounted owners and borrow the actual reference later.

Yeah, I figured that I pretty much have to use a proxy struct with RefCounted, and have the struct dtor do the actual cleanup of the class reference, something like:

	class Resource {
		void cleanup(); // inheritable
	}
	struct Proxy {
		private Resource res;
		this(Resource _res) { res = _res; }
		~this() { res.cleanup(); }
	}
	...
	auto obj = RefCounted!Proxy(allocateResource());


T

-- 
The problem with the world is that everybody else is stupid.
December 21
On 12/20/17 9:57 PM, Mike Franklin wrote:
> "Don't expect class destructors to be called at all by the GC"
> 
> I was a bit shocked to read that here: https://p0nce.github.io/d-idioms/#The-trouble-with-class-destructors
> 
> The document tries to clarify with:
> "The garbage collector is not guaranteed to run the destructors for all unreferenced objects."
> 
> Unfortunately, that doesn't really shed much light on this oddity.  So, specifically, under what circumstances are destructors not called?

It's implementation defined :)

The gist is, you cannot expect that destructors will be run in a timely manner, or at all.

They may be called, and most of the time they are. But the language nor the current implementation makes a guarantee that they will be called.

For this reason, any classes that use non-memory resources should clean up those resources before becoming garbage. This is why most of the time, such items are managed by structs.

Note that the same non-guarantee exists in other GC'd languages, such as Java or C#.

-Steve
« First   ‹ Prev
1 2 3