www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - class destructors must be disabled?

reply =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
I am writing these in view of class objects commonly being constructed 
as GC-owned objects.

GUIDELINES:

We've seen the following points mentioned in these forums.

- (This one is just a complication.) Classes don't have destructors 
anyway; they should be called finalizers.

- Don't allocate memory from the GC in a class destructor. (For example, 
writeln may be fine with simple types but writefln or format would not be.)

- Don't touch any GC-owned member in a class destructor because that 
member may have already been finalized.

SOME FACTS:

- Class destructors are not guarenteed to be executed. And this cannot 
be controlled by the programmer. For example, the user of the program 
can pass the following command line option and the GC will not execute 
class destructors:

   $ myprogram "--DRT-gcopt=cleanup:none" ...

(The following example will run fine when started that way.)

- Even if the destructor is executed by the GC, when it is executed 
exactly is naturally unspecified ("sometime in the future"). For that 
reason, it does not make sense to leave important responsibilities to 
class destructors like closing a File. Not only the file may not be 
closed, you may be exceeding resources that the OS provides by holding 
on to them for too long.

HORROR:

Now, a big one that I've just realized.

- The two guidelines above (the "don't touch class members" one and the 
"don't allocate" one) miss an important fact that they apply recursively 
even to struct members of classes. For example, structs that are used as 
members of a class cannot allocate memory in their destructors either.

The following program ends with core.exception.InvalidMemoryOperationError:

import std.format;

void closeConnection(string s) {
}

struct S {
   int id;

   ~this() {
     closeConnection(format!"%s signing off"(id));
   }
}

class C {
   S s;

   this(int id) {
     s = S(id);
   }

   //  disable ~this();
}

void main() {
   // Just 1 is sufficient to show the error in Ali's environment.
   enum N = 1;
   foreach (i; 0 .. N) {
     auto c = new C(i);
   }
}

Note how the struct is written in good faith and the class is obeying 
all the guidelines (by not even defining a destructor). I think the 
problem is the missed guideline that is on the subject line: classes 
should  disable their destructors. The program above will work fine when 
that line is uncommented.

Of course, we still should and do have the power to shape our programs 
any way we want but I think ' disable ~this();' should be added to 
classes as a general rule unless the programmer knows it will work 
otherwise.

What do you think?

Ali
May 18 2022
next sibling parent reply Steven Schveighoffer <schveiguy gmail.com> writes:
On 5/18/22 2:02 PM, Ali Çehreli wrote:

 Of course, we still should and do have the power to shape our programs 
 any way we want but I think ' disable ~this();' should be added to 
 classes as a general rule unless the programmer knows it will work 
 otherwise.
 
 What do you think?
No. Class destructors are for cleaning up non-GC resources. As long as you stick to those, you can safely run them. Structs that get put into classes have to run their destructors properly, otherwise, you will have horrible inconsistencies. For instance, I would not want to disable the destruction of a RefCounted struct inside a class. You can use the GC.inFinalizer to check if you are concerned about using the GC in your struct dtors. -Steve
May 18 2022
next sibling parent reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Wed, May 18, 2022 at 02:35:00PM -0400, Steven Schveighoffer via
Digitalmars-d-learn wrote:
[...]
 No. Class destructors are for cleaning up non-GC resources. As long as
 you stick to those, you can safely run them.
 
 Structs that get put into classes have to run their destructors
 properly, otherwise, you will have horrible inconsistencies.
 
 For instance, I would not want to disable the destruction of a
 RefCounted struct inside a class.
[...] So if the user runs your program with --DRT-gcopt=cleanup:none and you happen to have a RefCounted struct inside a GC-allocated class, then you're screwed? T -- Three out of two people have difficulties with fractions. -- Dirk Eddelbuettel
May 18 2022
parent reply Steven Schveighoffer <schveiguy gmail.com> writes:
On 5/18/22 2:58 PM, H. S. Teoh wrote:
 On Wed, May 18, 2022 at 02:35:00PM -0400, Steven Schveighoffer via
Digitalmars-d-learn wrote:
 [...]
 No. Class destructors are for cleaning up non-GC resources. As long as
 you stick to those, you can safely run them.

 Structs that get put into classes have to run their destructors
 properly, otherwise, you will have horrible inconsistencies.

 For instance, I would not want to disable the destruction of a
 RefCounted struct inside a class.
[...] So if the user runs your program with --DRT-gcopt=cleanup:none and you happen to have a RefCounted struct inside a GC-allocated class, then you're screwed?
I approach it from a different way. Let's say I'm writing a File class, and it has a file descriptor inside it. It's not me that's deciding when to clean up the object. All I want to do as the *library author* is to clean up the resource *I* opened, when someone is cleaning my object up. In other words, I don't care how you destroy it, GC, synchronously, etc, when you clean up the class instance, I will clean up my mess. That's just good lifetime management. Not cleaning it up because you're afraid of destructors is not the answer. That being said, I think it's a reasonable position for you to not support running your program with that DRT option (obviously, we do rely on the GC to clean up some things). -Steve
May 18 2022
parent reply =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
On 5/18/22 12:45, Steven Schveighoffer wrote:

 Not cleaning it up because you're afraid of destructors is not the 
answer. Fine. The GC allocation issue remains. It looks like one of the following should be observed: a) disable ~this() for all classes so that we are safe from the destructors of otherwise-well-written struct members as well. In general, this option requires a cleanup function for the class, which should be called at an opportune moment before its memory is freed by the GC. b) Consider GC.inFinalizer() in every struct destructor that has any chance of being used in a class. This may not work for all structs (see below). c) Extend the "do not allocate GC memory in the destructor" guideline to all structs. Do not allocate memory in such conditions, which may be problematic because my example would make every struct object expensive by holding on to a string prepared before hand to be used in the destructor: struct S { int id; string closingMessage; // <-- Expensive this(int id) { this.id = id; this.closingMessage = format!"%s signing off"(id); } ~this() { // No allocation in the destructor; good. closeConnection(closingMessage); } } The non-expensive solution is to use a nogc solution like sprintf? Hmm. Perhaps the guideline should be "all destructors must be nogc". Ali
May 18 2022
next sibling parent Steven Schveighoffer <schveiguy gmail.com> writes:
On 5/18/22 3:58 PM, Ali Çehreli wrote:

 Hmm. Perhaps the guideline should be "all destructors must be  nogc".
That one I agree with. -Steve
May 18 2022
prev sibling parent bauss <jj_1337 live.dk> writes:
On Wednesday, 18 May 2022 at 19:58:09 UTC, Ali Çehreli wrote:
 Hmm. Perhaps the guideline should be "all destructors must be 
  nogc".

 Ali
It should probably just default to that and with no exception, since you will never end up in a situation where you don't want nogc for a destructor. At least I can't imagine one, and if there is one then it's probably due to another design issue in one's program.
May 18 2022
prev sibling parent =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
On 5/18/22 11:35, Steven Schveighoffer wrote:

 Structs that get put into classes have to run their destructors
 properly, otherwise, you will have horrible inconsistencies.
Does that suggest a different guideline: "Careful with structs in classes." That goes against orthogonality (independence). I should not care where a struct type is used. Or, the way to use it properly should be handled by the struct. (Continuing below.)
 You can use the GC.inFinalizer to check if you are concerned about using
 the GC in your struct dtors.
Is that the guideline for most usability then? struct destructors must check for GC.inFinalizer because they may be used in a class. This doesn't sound useful either. Perhaps my struct example should have allocated the "farewell" string before the destructor. And then this renders struct destructors almost like class destructors: Don't allocate in the destructor. Something is fishy here. :) Ali
May 18 2022
prev sibling parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Wed, May 18, 2022 at 11:02:01AM -0700, Ali Çehreli via Digitalmars-d-learn
wrote:
[...]
 HORROR:
 
 Now, a big one that I've just realized.
 
 - The two guidelines above (the "don't touch class members" one and
 the "don't allocate" one) miss an important fact that they apply
 recursively even to struct members of classes. For example, structs
 that are used as members of a class cannot allocate memory in their
 destructors either.
[...]
 Of course, we still should and do have the power to shape our programs
 any way we want but I think ' disable ~this();' should be added to
 classes as a general rule unless the programmer knows it will work
 otherwise.
[...] I remember years ago, Andrei proposed that we remove class dtors (or finalizers, whatever) from the language altogether. That may be a bit extreme, but it reflects the general idea you wrote here. One observation about class dtors is, if they're useless for cleaning up resources (since we can't rely on them being run at all, and we're not allowed to touch anything referenced by the class that may have already been collected by the GC, and we're not allowed to do anything that may trigger a GC allocation), then *what practical use do they serve at all*?! At this point they're just some vestigial construct that are not useful for all practical intents and purposes anymore. The one big caveat, of course, is emplaced classes and classes whose allocation is being managed by something other than the (default) GC. If a class is stack-allocated, for example, the dtor could meaningfully do something useful like free resources upon going out of scope. IOW, class dtors are useless by default, and only useful in non-default usage. :-P T -- Obviously, some things aren't very obvious.
May 18 2022