Thread overview
class destructors must be @disabled?
May 18, 2022
Ali Çehreli
May 18, 2022
H. S. Teoh
May 18, 2022
Ali Çehreli
May 19, 2022
bauss
May 18, 2022
Ali Çehreli
May 18, 2022
H. S. Teoh
May 18, 2022
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

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
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
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
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

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
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

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 19, 2022
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.