May 24, 2012
On Thursday, 24 May 2012 at 17:57:11 UTC, Steven Schveighoffer wrote:
> On Thu, 24 May 2012 13:47:31 -0400, Tove <tove@fransson.se> wrote:
>
> There's a big problem with this though.  Your destructor *has no idea* whether it's being called from within a collection cycle, or from clear.  You must assume the most restrictive environment, i.e. that the dtor is being called from the GC.
>
> This is even true with struct dtors!
>
> -Steve

If there is a clear location where a manual close() function can be called... then there are many safe solutions to automatically and safely call clear instead.

std.typecons.Unique

If you are a library creator, you could even use a factory to enforce wrapping in Unique...  But I don't see any point of adding a non standard destructor function name, there are numerous ways to facilitate RAII.

May 24, 2012
On Thursday, 24 May 2012 at 17:57:11 UTC, Steven Schveighoffer wrote:
> On Thu, 24 May 2012 13:47:31 -0400, Tove <tove@fransson.se> wrote:
>
>> On Thursday, 24 May 2012 at 17:06:19 UTC, ponce wrote:
>>> I really had a hard time to believe it when #D told me so, but there is no guaranteed order of destruction and as you cannot relies on members still being alive in a class destructor.
>>> All of it can happen when making absolutely no cycles in the object graph.
>>>
>>> What I do now is having a close function for each class which hold a non-memory resource.
>>>
>>> This is writtent in TDPL, but I wish I was told earlier :)
>>
>> http://dlang.org/class.html#destructors
>>
>> "This rule does not apply to auto objects or objects deleted with the DeleteExpression, as the destructor is not being run by the garbage collector, meaning all references are valid."
>>
>> i.e. non gc resources are fine... and it's also fine if you call clear()... it's only a problem if you rely on automatic collection and reference a object... so there's no need for close, as clear() will do the trick.
>
> There's a big problem with this though.  Your destructor *has no idea* whether it's being called from within a collection cycle, or from clear.  You must assume the most restrictive environment, i.e. that the dtor is being called from the GC.
>
> This is even true with struct dtors!
>
> -Steve

Looks to me like an issue with separation of concerns. I think that dtors need to only provide deterministic management of resources and not affect GC algorithms:
1. classes should *not* have dtors at all.
2. struct values should *not* be gc managed [*].

Composition of classes and structs should be handled as follows:
1. If a class contains a pointer to a struct it doesn't scan it in a GC cycle. The runtime can provide a hook so that structs could register a callback for RC purposes.
2. When a class contains a strcut value, they share the same lifetime thus the GC will call the struct's dtor when the object is collected.
3. If a struct contains a reference to an object (class instance) than *that* object instance is scanned by the GC.

[*] point 3 above means that struct pointers are scanned in a pass-thru way only for the purpose of scanning contained object references.

With the above semantics, the dtor does know that all its members (except for class references) are valid, including any pointers to other structs and has similar semantics to c++ dtors. This scheme can be enforced by the compiler and is safe since objects won't inherently hold any resources by themselves (they don't have dtors). This also allows to implement more advanced finalization schemes (e.g. dependencies between resources).

May 24, 2012
On Thursday, 24 May 2012 at 19:46:07 UTC, foobar wrote:
> Looks to me like an issue with separation of concerns. I think that dtors need to only provide deterministic management of resources and not affect GC algorithms:
> 1. classes should *not* have dtors at all.
> 2. struct values should *not* be gc managed [*].
>

Why not simply set "BlkAttr.NO_SCAN" on ourselves if we need certain resources in the destructor? Assuming we one day get user defined attributes, it can be make quite simple...

May 24, 2012
On Thursday, 24 May 2012 at 20:53:33 UTC, Tove wrote:
> On Thursday, 24 May 2012 at 19:46:07 UTC, foobar wrote:
>> Looks to me like an issue with separation of concerns. I think that dtors need to only provide deterministic management of resources and not affect GC algorithms:
>> 1. classes should *not* have dtors at all.
>> 2. struct values should *not* be gc managed [*].
>>
>
> Why not simply set "BlkAttr.NO_SCAN" on ourselves if we need certain resources in the destructor? Assuming we one day get user defined attributes, it can be make quite simple...

Tested my idea... unfortunately it's broken...

GC.collect() while the program is running, is OK... so I was hoping to add:
GC.disable() just before main() ends, but apparently this request is ignored.

i.e. back to square 1, undefined collecting order once the program exits.

import std.stdio;
import core.memory;

class Important
{
  this()
  {
    us ~= this;
  }
  ~this()
  {
    writeln("2");
  }

private:
  static Important[] us;
}

class CollectMe
{
  Important resource;

  this()
  {
    resource = new Important();
  }
  ~this()
  {
    writeln("1");
    clear(resource);
  }
}

void main()
{
  GC.setAttr(cast(void*)new CollectMe(), GC.BlkAttr.NO_SCAN);
  GC.collect();
  GC.disable();

  writeln("3");
}
May 25, 2012
On Thursday, 24 May 2012 at 13:49:38 UTC, Steven Schveighoffer wrote:
> You actually need a finalizer if you want to have resources that aren't GC allocated.


Does that ring a bell? ;)
May 25, 2012
On Thursday, 24 May 2012 at 16:06:23 UTC, Andrei Alexandrescu wrote:
> It does matter because a destructor may use an object that has just been destroyed.
>
> Andrei


Andrei: .NET has this exact problem, and handles it pretty well.

There are two types of "Dispose()": manual and automatic.

Whenever you're wrapping some unmanaged resource (e.g. file handle), you wrap it inside some managed object (e.g. SafeFileHandle).

Then you embed _that_ resource in the actual object you want (e.g. FileStream).

Now, there are two ways a FileStream can get destroyed:

1. Through a manual call to FileStream.Dispose(). In this case, all embedded objects (e.g. SafeFileHandle) are *guaranteed* to be valid, so we simply flush the file and call SafeFileHandle.Dispose() to dispose of the managed resources, and then dispose of all the unmanaged resources (which are primitive fields, guaranteed to be accessible). Furthermore, the object suppresses its own finalizer.

2. Through a garbage-collected call to ~FileStream(). In this case, the managed resources such as SafeFileHandle will be (or is already) destroyed SEPARATELY, and so we do _NOT_ access them. We ONLY dispose of the unmanaged resources, if any, and let the managed resources take care of themselves.


It's a pretty well-defined sequence, and it works well in practice.

(Of course, you don't actually _need_ this double-indirection here: You could instead just wrap the unmanaged resource manually, and do everything in FileStream. The reason for the double-indirection is something slightly unrelated. I was just explaining how to take care of the managed resource disposal problem that you mentioned.)


You could point out that, in this case, the FileStream doesn't flush its buffers  before the file handle is destroyed, if the GC collects the object.

That problem is solvable in two ways, although .NET simply chose to not worry about it, as far as I know:

1. Simply wrap the handle inside FileStream. Since it will be unmanaged, you can access it during disposal.

2. If that isn't possible, keep a _strong_, *unmanaged* reference to your _managed_ SafeFileHandle object. (This is accomplished through acquiring a cookie from the GC.) Because of this, SafeFileHandle will NOT be destroyed before FileStream. You can then use this fact to access SafeFileHandle inside FileStream's finalizer, through the unmanaged (but safe) cookie.



tl;dr: It's a completely solved problem in .NET; there really shouldn't be any issues with it in D either.
May 25, 2012
On 2012-05-24 21:46, foobar wrote:

> Looks to me like an issue with separation of concerns. I think that
> dtors need to only provide deterministic management of resources and not
> affect GC algorithms:
> 1. classes should *not* have dtors at all.
> 2. struct values should *not* be gc managed [*].
>
> Composition of classes and structs should be handled as follows:
> 1. If a class contains a pointer to a struct it doesn't scan it in a GC
> cycle. The runtime can provide a hook so that structs could register a
> callback for RC purposes.
> 2. When a class contains a strcut value, they share the same lifetime
> thus the GC will call the struct's dtor when the object is collected.

How is that any different than having destructors for classes.

-- 
/Jacob Carlborg
May 25, 2012
On Friday, 25 May 2012 at 07:13:11 UTC, Jacob Carlborg wrote:
> On 2012-05-24 21:46, foobar wrote:
>
>> Looks to me like an issue with separation of concerns. I think that
>> dtors need to only provide deterministic management of resources and not
>> affect GC algorithms:
>> 1. classes should *not* have dtors at all.
>> 2. struct values should *not* be gc managed [*].
>>
>> Composition of classes and structs should be handled as follows:
>> 1. If a class contains a pointer to a struct it doesn't scan it in a GC
>> cycle. The runtime can provide a hook so that structs could register a
>> callback for RC purposes.
>> 2. When a class contains a strcut value, they share the same lifetime
>> thus the GC will call the struct's dtor when the object is collected.
>
> How is that any different than having destructors for classes.

It makes the call order deterministic like in C++.

e.g.
class Foo {}

struct A {
  resource r;
  ~this() { release(r); }
}

struct B {
  A* a;
  Foo foo;
  ~this() { delete a; } // [1]
}

Lets look at point [1]:
The "foo" instance is "managed" by the GC since the only resource it holds is memory. The "a" member wraps some "non-managed" resource (e.g. file descriptor) and in this model is still valid, thus allows me to deterministically dispose of it as in c++.

This can be simply checked at compile-time - you can only reference non class instance members in the destructor, so adding a "delete foo;" statement at point [1] simply won't compile.
May 25, 2012
On Thu, 24 May 2012 18:57:11 +0100, Steven Schveighoffer <schveiguy@yahoo.com> wrote:
> On Thu, 24 May 2012 13:47:31 -0400, Tove <tove@fransson.se> wrote:
>
>> On Thursday, 24 May 2012 at 17:06:19 UTC, ponce wrote:
>>> I really had a hard time to believe it when #D told me so, but there is no guaranteed order of destruction and as you cannot relies on members still being alive in a class destructor.
>>> All of it can happen when making absolutely no cycles in the object graph.
>>>
>>> What I do now is having a close function for each class which hold a non-memory resource.
>>>
>>> This is writtent in TDPL, but I wish I was told earlier :)
>>
>> http://dlang.org/class.html#destructors
>>
>> "This rule does not apply to auto objects or objects deleted with the DeleteExpression, as the destructor is not being run by the garbage collector, meaning all references are valid."
>>
>> i.e. non gc resources are fine... and it's also fine if you call clear()... it's only a problem if you rely on automatic collection and reference a object... so there's no need for close, as clear() will do the trick.
>
> There's a big problem with this though.  Your destructor *has no idea* whether it's being called from within a collection cycle, or from clear.  You must assume the most restrictive environment, i.e. that the dtor is being called from the GC.
>
> This is even true with struct dtors!

The C# dispose model suggests/gives examples of handling this sort of problem using a bool and 2 dispose methods, see:
http://msdn.microsoft.com/en-us/library/fs2xkftw.aspx

Could we do something similar in D, i.e. provide a template class which could wrap any reference and implement this..

R

-- 
Using Opera's revolutionary email client: http://www.opera.com/mail/
May 25, 2012
On 2012-05-25 11:02, foobar wrote:

> It makes the call order deterministic like in C++.
>
> e.g.
> class Foo {}
>
> struct A {
> resource r;
> ~this() { release(r); }
> }
>
> struct B {
> A* a;
> Foo foo;
> ~this() { delete a; } // [1]
> }

I though we were talking about classes holding structs:

class B {
    A* a;
    Foo foo;
    ~this() { delete a; }
}

In this case you don't know when/if the destructor of B is called. It doesn't help to wrap it in a struct, you could just have put it directly in A. Is that correct?

> Lets look at point [1]:
> The "foo" instance is "managed" by the GC since the only resource it
> holds is memory. The "a" member wraps some "non-managed" resource (e.g.
> file descriptor) and in this model is still valid, thus allows me to
> deterministically dispose of it as in c++.

Ok, but if B is a class?

> This can be simply checked at compile-time - you can only reference non
> class instance members in the destructor, so adding a "delete foo;"
> statement at point [1] simply won't compile.

If you have a pointer to a struct you don't know how it was created. It's possible it's been created with "new", which means the garbage collector needs to delete it.

-- 
/Jacob Carlborg
1 2 3 4 5 6 7
Top | Discussion index | About this forum | D home