www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Class destructors - clarify what is safe

reply Brother Bill <brotherbill mail.com> writes:
On page 325 of Programming in D, we may have a single 
parameterless destructor named ```~this()```.

A class may have members of string, File, other classes, etc.

When entering a class destructor, are any of the dynamically 
allocated class members safe to read?  That is, can we expect 
that a class instance entering its destructor is "fully intact", 
satisfying all its invariants, and all member variables have not 
undergone any destruction at this point?

I would expect that a class with strings, FILE handles, etc. 
would need these to do its housekeeping.  If we opened a file in 
the constructor, we should be able to close the file in the 
destructor.

As we can explicitly call destroy() on a class instance multiple 
times, do we need to be "careful" of only closing a file once, 
rather than closing a file multiple times, if that can cause 
issues.

What guarantees, if any, exist on entering a class destructor?
Feb 14
next sibling parent reply "H. S. Teoh" <hsteoh qfbox.info> writes:
On Sat, Feb 14, 2026 at 05:36:32PM +0000, Brother Bill via Digitalmars-d-learn
wrote:
 On page 325 of Programming in D, we may have a single parameterless
 destructor named ```~this()```.
 
 A class may have members of string, File, other classes, etc.
 
 When entering a class destructor, are any of the dynamically allocated
 class members safe to read?
No. The order in which the GC deallocates objects is not defined, and when the dtor is run, any references to GC-allocated objects cannot be safely read, as those objects may have already been destroyed.
 I would expect that a class with strings, FILE handles, etc. would
 need these to do its housekeeping.  If we opened a file in the
 constructor, we should be able to close the file in the destructor.
Files in general would still be valid to read, because they are not managed by the GC, but by the OS. This is the whole point of dtors. However, be careful of class wrappers around files. If your class references such a wrapper, the wrapper may have already been destructed by the time your dtor runs, so it's not safe to dereference it.
 As we can explicitly call destroy() on a class instance multiple
 times, do we need to be "careful" of only closing a file once, rather
 than closing a file multiple times, if that can cause issues.
Calling .destroy on a class object more than once may trigger UB.
 What guarantees, if any, exist on entering a class destructor?
Nothing allocated by the GC is guaranteed to be valid when a dtor is called. Everything else (e.g. objects allocated by malloc() in libc) should still be safe to access. // Basically, when something is allocated on the GC, the GC takes full responsibility for managing its lifetime. User code should not try to intervene. If your object needs to be managed manually, don't allocate it from the GC, use C's malloc() or your own memory allocation scheme. Because of this, class dtors really should not be necessary if you only allocate objects from the GC. They are only necessary when you need to manage resources that are not allocated by the GC, such as OS file handles, memory allocated by C's malloc(), or other such things. The dtor should only take care of cleaning up these external resources, and should not try to do anything related to GC-allocated objects. T -- Ignorance is bliss... until you suffer the consequences!
Feb 14
next sibling parent reply Brother Bill <brotherbill mail.com> writes:
On Saturday, 14 February 2026 at 18:42:21 UTC, H. S. Teoh wrote:
 Basically, when something is allocated on the GC, the GC takes 
 full responsibility for managing its lifetime. User code should 
 not try to intervene.  If your object needs to be managed 
 manually, don't allocate it from the GC, use C's malloc() or 
 your own memory allocation scheme. Because of this, class dtors 
 really should not be necessary if you only allocate objects 
 from the GC.  They are only necessary when you need to manage 
 resources that are not allocated by the GC, such as OS file 
 handles, memory allocated by C's malloc(), or other such 
 things.  The dtor should only take care of cleaning up these 
 external resources, and should not try to do anything related 
 to GC-allocated objects.


 T
How does one go about determining if these are GC-allocated objects, or if these are external to the GC?
Feb 14
parent "H. S. Teoh" <hsteoh qfbox.info> writes:
On Sat, Feb 14, 2026 at 08:49:08PM +0000, Brother Bill via Digitalmars-d-learn
wrote:
 On Saturday, 14 February 2026 at 18:42:21 UTC, H. S. Teoh wrote:
 Basically, when something is allocated on the GC, the GC takes full
 responsibility for managing its lifetime. User code should not try
 to intervene.  If your object needs to be managed manually, don't
 allocate it from the GC, use C's malloc() or your own memory
 allocation scheme.  Because of this, class dtors really should not
 be necessary if you only allocate objects from the GC.  They are
 only necessary when you need to manage resources that are not
 allocated by the GC, such as OS file handles, memory allocated by
 C's malloc(), or other such things.  The dtor should only take care
 of cleaning up these external resources, and should not try to do
 anything related to GC-allocated objects.
[...]
 How does one go about determining if these are GC-allocated objects,
 or if these are external to the GC?
You, the programmer, should know. :-D If you used `new` to allocate them, then they're allocated by the GC. Or if you used GC.malloc or any of the GC methods from core.memory.GC. If you allocated the objects using C's malloc(), then it's not GC-allocated. If it's a handle returned to you by the OS, then it's not GC-allocated. Etc. T -- Winners never quit, quitters never win. But those who never quit AND never win are idiots.
Feb 14
prev sibling next sibling parent reply Paul Backus <snarwin gmail.com> writes:
On Saturday, 14 February 2026 at 18:42:21 UTC, H. S. Teoh wrote:
 As we can explicitly call destroy() on a class instance 
 multiple times, do we need to be "careful" of only closing a 
 file once, rather than closing a file multiple times, if that 
 can cause issues.
Calling .destroy on a class object more than once may trigger UB.
This is technically true, in the sense that calling any function could conceivably trigger UB, but destructors *should* be idempotent, and any destructor marked as safe *must* be idempotent.
Feb 14
parent reply "H. S. Teoh" <hsteoh qfbox.info> writes:
On Sat, Feb 14, 2026 at 10:03:17PM +0000, Paul Backus via Digitalmars-d-learn
wrote:
 On Saturday, 14 February 2026 at 18:42:21 UTC, H. S. Teoh wrote:
 As we can explicitly call destroy() on a class instance multiple
 times, do we need to be "careful" of only closing a file once,
 rather than closing a file multiple times, if that can cause
 issues.
Calling .destroy on a class object more than once may trigger UB.
This is technically true, in the sense that calling any function could conceivably trigger UB, but destructors *should* be idempotent, and any destructor marked as safe *must* be idempotent.
True. So any dtor that closes a file should set its handle to a null value so that any subsequent calls will be a no-op. --T
Feb 14
parent reply =?UTF-8?Q?Ali_=C3=87ehreli?= <acehreli yahoo.com> writes:
On 2/14/26 4:46 PM, H. S. Teoh wrote:
 On Sat, Feb 14, 2026 at 10:03:17PM +0000, Paul Backus via Digitalmars-d-learn
wrote:
 On Saturday, 14 February 2026 at 18:42:21 UTC, H. S. Teoh wrote:
 As we can explicitly call destroy() on a class instance multiple
 times, do we need to be "careful" of only closing a file once,
 rather than closing a file multiple times, if that can cause
 issues.
Calling .destroy on a class object more than once may trigger UB.
This is technically true, in the sense that calling any function could conceivably trigger UB, but destructors *should* be idempotent, and any destructor marked as safe *must* be idempotent.
True. So any dtor that closes a file should set its handle to a null value so that any subsequent calls will be a no-op. --T
The way I've learned to see it for decades now, initially from the point of view of C++, calling the destructor more than once should be a mistake. The reason is, when the destructor completes, the object is considered to be dead. Calling any non-static function on it should be considered a programming error. I know structs are different because they are set to their initial state after certain operations (e.g. I think destroy()) but in that case the object is still an object, just morphed to its .init value. What do others think? Ali
Feb 17
parent "H. S. Teoh" <hsteoh qfbox.info> writes:
On Tue, Feb 17, 2026 at 01:52:57PM -0800, Ali Çehreli via Digitalmars-d-learn
wrote:
 On 2/14/26 4:46 PM, H. S. Teoh wrote:
 On Sat, Feb 14, 2026 at 10:03:17PM +0000, Paul Backus via Digitalmars-d-learn
wrote:
 On Saturday, 14 February 2026 at 18:42:21 UTC, H. S. Teoh wrote:
[...]
 Calling .destroy on a class object more than once may trigger UB.
This is technically true, in the sense that calling any function could conceivably trigger UB, but destructors *should* be idempotent, and any destructor marked as safe *must* be idempotent.
True. So any dtor that closes a file should set its handle to a null value so that any subsequent calls will be a no-op.
[...]
 The way I've learned to see it for decades now, initially from the
 point of view of C++, calling the destructor more than once should be
 a mistake.
 
 The reason is, when the destructor completes, the object is considered
 to be dead. Calling any non-static function on it should be considered
 a programming error.
I agree with this, in principle. Now, idempotent operations can also be helpful in some situations. It lets you elide verbose checks (like pervasive null checks peppering the code) by letting the caller ignore the exact state of an object because it's harmless (and does not change the result) to perform the operation twice. Idempotent destruction is questionable, however. If a dtor is called twice, then something smells bad, even if it's technically not wrong. It's the same as if a ctor were called twice to construct an object. The whole point of a ctor is to start from an invalid initial state and initialize the object into a valid state. Once that has been done, the object is already in a valid state and should not be initialized again -- the ctor code was written with the assumption that it wasn't in a valid state to start with. Violating this breaks the assumption, and this can lead to nasty bugs. Similarly, a dtor is written under the assumption that the object is in a valid state. After a dtor is finished the object is in an *invalid* state; calling it again breaks this assumption, which means it's liable to lead to nasty bugs. If druntime causes a ctor to run twice under any circumstances, I'd consider it a bug. Similarly, if a dtor were to run twice on the same object, I'd consider that a bug. T -- Roman numerals are difficult; but when you get to 159, it just CLIX.
Feb 17
prev sibling parent reply Forum User <forumuser example.com> writes:
On Saturday, 14 February 2026 at 18:42:21 UTC, H. S. Teoh wrote:
 When entering a class destructor, are any of the dynamically 
 allocated class members safe to read?
No. The order in which the GC deallocates objects is not defined, and when the dtor is run, any references to GC-allocated objects cannot be safely read, as those objects may have already been destroyed.
Does this deviate from the way C++ handles destruction? https://stackoverflow.com/questions/20045904/c-can-i-use-this-safely-in-a-destructor Quote: " Can I use this in a destructor Yes. For example, I know I'm not supposed to do anything with base classes, since they are gone. No, the base classes are still intact at this point. Members (and perhaps other base classes) of derived classes have already been destroyed, but members and base classes of this class remain until after the destructor has finished." I thought that also in D all the members and the base objects of the object which is destructed are still valid and accessible until the destructor finished.
Feb 14
parent reply "H. S. Teoh" <hsteoh qfbox.info> writes:
On Sun, Feb 15, 2026 at 12:58:14AM +0000, Forum User via Digitalmars-d-learn
wrote:
[...]
 I thought that also in D all the members and the base objects of the
 object which is destructed are still valid and accessible until the
 destructor finished.
Non-GC-allocated members are still valid until the dtor is done. But any class references and references to GC-allocated objects are not guaranteed to be valid, because the order in which the GC calls class dtors is not guaranteed. Basically, the GC finds the set of dead objects in an unspecified order, which from the POV of user code should be regarded as random and unpredictable. Consider an object c of class C: ``` class C { A a; B b; this(A _a, B _b) { a = _a; b = _b; } ~this() { } } auto a = new A; auto b = new B; auto c = new C(a, b); ``` When c goes out of scope, a and b are still referenced by c, but are no longer reachable from the rest of the program. The order in which the GC finds dead objects is not specified and up to the specifics of its implementation, so it could very well be that it first discovers that a is no longer reachable, and then discovers that c is unreachable. Depending on the implementation, it may call a's dtor first, free it, and then call c's dtor, then afterwards discover that c was holding the last reference to b, so b should also be collected, and therefore b's dtor is called last. This means that when c's dtor is run, a is no longer a valid reference (its dtor has run and its memory has be reclaimed by the GC, and potentially used for a new, unrelated object). So it's unsafe for c's dtor to access a. Of course, b is still valid because the GC hasn't discovered that it's dead (yet). But that doesn't mean that it will always happen this way. Depending on implementation details like the GC algorithm, the specific memory addresses of objects, etc., it could be that in a different GC collection cycle, b gets collected before c. So it's also unsafe to reference b from c's dtor -- it might coincidentally work this time round, but next time it may cause the program to crash. // In short, inside a class dtor you should NOT assume that any reference to a GC-allocated object is still valid. There is NO guarantee that referenced objects get collected before/after the referencing object. It can happen in any arbitrary order. (Any such guarantee wouldn't make sense in the context of a GC anyway, because it'd prevent the GC from collecting cycles properly. If A references B and B also references A, then which dtor should be called first?) This essentially limits the usefulness of dtors to only cleaning up external resources not managed by the GC. For the cleanup of all GC-allocated objects, user code should not try to (and does not need to) clean up manually. That's the GC's job. GC collection can reclaim objects in any arbitrary order, and user code should not rely on any specific order. If you need to control the order of collection, you should not use the GC; you should use C's malloc() or some other memory management mechanism. T -- It only takes one twig to burn down a forest.
Feb 14
parent reply Forum User <forumuser example.com> writes:
On Sunday, 15 February 2026 at 01:36:55 UTC, H. S. Teoh wrote:
 Basically, the GC finds the set of dead objects in an 
 unspecified order, which from the POV of user code should be 
 regarded as random and unpredictable.  Consider an object c of 
 class C:

 ```
 	class C {
 		A a;
 		B b;
 		this(A _a, B _b) { a = _a; b = _b; }
 		~this() {
 		}
 	}

 	auto a = new A;
 	auto b = new B;
 	auto c = new C(a, b);
 ```

 When c goes out of scope, a and b are still referenced by c, 
 but are no longer reachable from the rest of the program.  The 
 order in which the GC finds dead objects is not specified
Is it possible that during stack unwinding the GC kicks in and destroys an exception object before it is caught?
Feb 15
parent "H. S. Teoh" <hsteoh qfbox.info> writes:
On Sun, Feb 15, 2026 at 09:30:12AM +0000, Forum User via Digitalmars-d-learn
wrote:
[...]
 Is it possible that during stack unwinding the GC kicks in and
 destroys an exception object before it is caught?
No, why would it? An exception in flight is not a dead object (otherwise the catch block wouldn't be able to catch it), it will not be collected. T -- Perhaps the most widespread illusion is that if we were in power we would behave very differently from those who now hold it---when, in truth, in order to get power we would have to become very much like them. -- Unknown
Feb 15
prev sibling parent Steven Schveighoffer <schveiguy gmail.com> writes:
On Saturday, 14 February 2026 at 17:36:32 UTC, Brother Bill wrote:
 On page 325 of Programming in D, we may have a single 
 parameterless destructor named ```~this()```.

 A class may have members of string, File, other classes, etc.

 When entering a class destructor, are any of the dynamically 
 allocated class members safe to read?  That is, can we expect 
 that a class instance entering its destructor is "fully 
 intact", satisfying all its invariants, and all member 
 variables have not undergone any destruction at this point?

 I would expect that a class with strings, FILE handles, etc. 
 would need these to do its housekeeping.  If we opened a file 
 in the constructor, we should be able to close the file in the 
 destructor.
The destructor of a class is basically for cleaning up non-GC resources. You do not need to (and should not) try to free GC resources in a class destructor (you have to be careful with struct dtors as well, as those can be on the heap).
 As we can explicitly call destroy() on a class instance 
 multiple times, do we need to be "careful" of only closing a 
 file once, rather than closing a file multiple times, if that 
 can cause issues.
In practice, any GC-allocated object which has a *direct* link to a non-GC resource (file descriptor, c-malloc'd data, etc) should clean up the resource. You should not go through a GC-allocated object unless you have pinned it. If you want reliable ordered cleanup, you should implement this separately. e.g. add a `close` method. -Steve
Feb 14