D's Destructors are What Scott Meyers Warned Us About

rikki cattermole rikki at cattermole.co.nz
Wed May 23 02:13:13 UTC 2018


On 23/05/2018 1:59 PM, sarn wrote:
> (I'm referring to Scott's 2014 DConf talk: 
> https://www.youtube.com/watch?v=KAWA1DuvCnQ)
> 
> I was actually preparing a DIP about this when Manu posted to the forums 
> about his own related problems with C++ interop.
> 
> I traced a bug in some of my D code to my own misunderstanding of how 
> D's destructors actually work.  So I did some research and discovered a 
> bunch of edge cases with using __dtor, __xdtor and 
> hasElaborateDestructor.  I tried reviewing the packages on 
> code.dlang.org and some elsewhere (thankfully only about 1% of D 
> packages use these functions directly) and it turns out I'm not the only 
> one confused.  I don't have time to file bug reports for every package, 
> so, if you're responsible for code that handles destructors manually, 
> please do a review.  There's a *lot* of buggy code out there.
> 
> I'm starting this thread to talk about ways to make things better, but 
> first the bad news.  Let's use this idiomatic code as an example of 
> typical bugs:
> 
> void foo(T)(auto ref T t)
> {
>      ...
>      static if (hasElaborateDestructor!T)
>      {
>          t.__dtor();
>      }
>      ...
> }
> 
> Gotcha #1:
> __dtor calls the destructor defined by T, but not the destructor defined 
> by any members of T (if T is a struct or class).  I know, some of you 
> might be thinking, "Silly sarn, that's what __xdtor is for, of course!"  
> Turns out this isn't that widely known or understood (__dtor is used in 
> examples in the spec --- 
> https://dlang.org/spec/struct.html#assign-overload).  A lot of code is 
> only __dtor-aware, and there's at least some code out there that checks 
> for both __dtor and __xdtor and mistakenly prefers __dtor.  __xdtor only 
> solves the specific problem of also destroying members.
> 
> Gotcha #2:
> The destructor will never be run for classes because 
> hasElaborateDestructor is only ever true for structs.  This is actually 
> per the documentation, but it's also not well known "on the ground" 
> (e.g., a lot of code has meaningless is(T == class) or is(T == struct) 
> around hasElaborateDestructor).  The code example is obviously a leak if 
> t was emplace()d in non-GC memory, but even for GCed classes, it's 
> important for containers to be explicit about whether or not they own 
> reference types.
> 
> Gotcha #3:
> Even if __dtor is run on a class instance, it generally won't run the 
> correct destructor because it's not virtual.  (I.e., if T is a base 
> class, no destructors for derived classes will be called.)  The spec 
> says that D destructors are virtual, but this is emulated using runtime 
> type information rather than by using the normal virtual function 
> implementation.  __xdtor is the same.
> 
> Gotcha #4:
> Even if the right derived class __dtor is run, it won't run the 
> destructors for any base classes.  The spec says that destructors 
> automatically recurse to base classes, but, again, this is handled 
> manually by walking RTTI, not by making the destructor function itself 
> recurse.
> 
> Gotcha #5:
> The idiom of checking if something needs destruction before destroying 
> it is often implemented incorrectly.  As before, hasElaborateDestructor 
> works for structs, but doesn't always work as expected for classes.  
> hasMember!(T, "__dtor") seems to work for classes, but doesn't work for 
> a struct that doesn't define a destructor, but requires destruction for 
> its members (a case that's easy to miss in testing).
> 
> It looks like most D programmers think that D destructors work like they 
> typically do in C++, just like I did.
> 
> Here are some recommendations:
> * Really try to just use destroy().  Manually working with 
> __dtor/__xdtor is a minefield, and I haven't seen any code that actually 
> reimplements the RTTI walk that the runtime library does.  
> (Unfortunately destroy() currently isn't zero-overhead for plain old 
> data structs because it's based on RTTI, but at least it works.)
> * Avoid trying to figure out if something needs destruction. Just 
> destroy everything, or make it clear you don't own classes at all, or be 
> totally sure you're working with plain old data structs.
> * Some code uses __dtor as a way to manually run cleanup code on an 
> object that will be used again.  Putting this cleanup code into a normal 
> method will cause fewer headaches.
> 
> The one other usage of these low-level destructor facilities is checking 
> if a type is a plain old data struct.  This is an important special case 
> for some code, but everyone seems to do the check a different way.  
> Maybe a special isPod trait is needed.

I would consider the current state with classes a bug.
So ticket please, it should not require a DIP to change (although Walter 
may disagree).



More information about the Digitalmars-d mailing list