On 5/22/18 9: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 think it's very good to bring attention to these pitfalls, thanks!
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.
This is advice you need to follow. Using the underlying __functions are
error prone, and subject to weird errors as you have found.
The other thing to do is to follow the example of what Phobos functions
do when dealing with these low-level functions.
In terms of classes, the RTTI walk is callable via the function
_rt_finalize (an extern(C) function, you can prototype it anywhere). I
highly recommend just calling destroy as it will do this.
(Unfortunately destroy() currently isn't zero-overhead for plain old
data structs because it's based on RTTI, but at least it works.)
Hm.. that should be fixed. I don't see why we can't just do = T.init, we
should at least be optimizing to this for small enough structs.
Please file an enhancement request.
* 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.
Ideally, destroy should do the most efficient thing. So this is good
advice as well.
* 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.
Using __dtor is a very bad idea in almost all cases. Putting cleanup
code into a normal function can have some of the same pitfalls, however
(what if you forget to call the super version of the method?). The only
*correct* way to destroy an object is to follow the runtime's example or
call the function directly.
The destructor also has the nice feature of being called when the struct
goes out of scope.
Best advice -- just use destroy on types to clean them up.
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.
isPod as you have described it is not difficult:
enum isPod(T) = is(T == struct) && !hasElaborateDestructor!T;
But this is not necessarily the definition of POD. Generally this means
it has no postblit, and some people may even be expecting such a thing
to have no methods as well. So I'm not sure we want to add such a
definition to the library.
Clearly destroy is preferred to checking or doing anything else, but
maybe in some cases we want a more efficient destruction that cuts
corners. For instance, there's no reason to blit the init value back to
the object when its memory is being reclaimed. That building block is
somewhat inextricable from destroy at the moment.
Ideally, you shouldn't have to worry about it as a normal user -- some
genius library author should take care of these details. Unfortunately,
the only place where this happens is on the stack or in the GC. All
other memory allocation schemes require you to get your hands dirty with
destruction.
-Steve