[edited to add notes and apply Howard Hinnant’s de-escalation to static_cast]

After my talk on Friday, a couple of people asked me how I was storing destructors in my gcpp library. Since several people are interested, I thought I’d write a note.

The short answer is to store two raw pointers: one to the object, and one to a type-erased destructor function that’s handy to write using a lambda.

In deferred_heap.h, you’ll see that the library stores a deferred destructor as:

struct destructor { const void* p; void(*destroy)(const void*); };

So we store two raw pointers, a raw pointer to the object to be destroyed and a raw function pointer to the destructor. Then later we can just invoke d.destroy(d.p).

The most common question is: “But how can you get a raw function pointer to the destructor?” I use a lambda:

// Called indirectly from deferred_heap::make<T>. // Here, t is a T&. dtors.push_back({ std::addressof(t), // address of object [](const void* x) { static_cast<const T*>(x)->~T(); } }); // dtor to invoke

A non-capturing lambda has no state so it can be used as a plain function, and because we know T here we just write a lambda that performs the correct cast back to invoke the correct T destructor. So for each distinct type T that this is instantiated with, we generate one T-specific lambda function (on demand at compile time, globally unique) and we store that function’s address.

Notes:

The lambda gives a handy way to do type erasure in this case. One line removes the type, and the other line adds it back.

Yes, it’s legal to call the destructor on a const object. It has to be; we have to be able to destroy const objects. Const never applies to a constructor or destructor, it applies to all the other member functions.

Some have asked what the body of the wrapper lambda generates. I looked at the code gen in Clang a couple of weeks ago, and depending on the optimization level, the lambda is generated as either a one-instruction function (just a single jmp to the actual destructor) or as a copy of the destructor if it’s inlined (no run-time overhead at all, just another inline copy of the destructor in the binary if it’s generally being inlined anyway).

Because we are storing the destructor at the same time as we are constructing the T object, we know the complete object’s type is T and therefore can easily store the correct destructor for the complete object. Later, regardless of whether the list deferred_ptr keeping it alive is a deferred_ptr<T> or something else, such as a deferred_ptr<const T> or deferred_ptr<BaseClass> or deferred_ptr<TDataMember>, the correct T destructor will be called to clean up the complete object.

I’ve now added a version of this to the gcpp readme FAQ section.