D's Destructors are What Scott Meyers Warned Us About
sarn
sarn at theartofmachinery.com
Wed May 23 01:59:50 UTC 2018
(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.
More information about the Digitalmars-d
mailing list