digitalmars.D.learn - Class destructors - clarify what is safe
- Brother Bill (17/17) Feb 14 On page 325 of Programming in D, we may have a single
- H. S. Teoh (27/41) Feb 14 No. The order in which the GC deallocates objects is not defined, and
- Brother Bill (3/16) Feb 14 How does one go about determining if these are GC-allocated
- H. S. Teoh (11/25) Feb 14 You, the programmer, should know. :-D If you used `new` to allocate
- Paul Backus (5/11) Feb 14 This is technically true, in the sense that calling any function
- H. S. Teoh (4/15) Feb 14 True. So any dtor that closes a file should set its handle to a null
- =?UTF-8?Q?Ali_=C3=87ehreli?= (11/29) Feb 17 The way I've learned to see it for decades now, initially from the point...
- H. S. Teoh (27/46) Feb 17 [...]
- Forum User (15/21) Feb 14 Does this deviate from the way C++ handles destruction?
- H. S. Teoh (60/63) Feb 14 Non-GC-allocated members are still valid until the dtor is done.
- Forum User (3/22) Feb 15 Is it possible that during stack unwinding the GC kicks in and
- H. S. Teoh (7/9) Feb 15 No, why would it? An exception in flight is not a dead object (otherwise
- Steven Schveighoffer (12/28) Feb 14 The destructor of a class is basically for cleaning up non-GC
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
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
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. THow does one go about determining if these are GC-allocated objects, or if these are external to the GC?
Feb 14
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
On Saturday, 14 February 2026 at 18:42:21 UTC, H. S. Teoh wrote: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.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.
Feb 14
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: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. --TThis 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.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.
Feb 14
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: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? AliOn Saturday, 14 February 2026 at 18:42:21 UTC, H. S. Teoh wrote: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. --TThis 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.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.
Feb 17
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:[...]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.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.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
On Saturday, 14 February 2026 at 18:42:21 UTC, H. S. Teoh wrote: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.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.
Feb 14
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
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
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
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









"H. S. Teoh" <hsteoh qfbox.info> 