Qwertypal Qwertypal - 2 months ago 6x
C++ Question

destructing an object with Incomplete type

I have read other stackoverflow questions on the subject, yet I am really confused with the incomplete type and this C++ specification paragraph ยง5.3.5/5 :

If the object being deleted has incomplete class type at the point of
deletion and the complete class has a non-trivial destructor or a
deallocation function, the behavior is undefined.

Given an example, .h :

template<class T> class my_scoped_ptr
T *t;
my_scoped_ptr(T * _t) : t(_t) {}
~my_scoped_ptr() {
typedef char type_must_be_complete[ sizeof(T)? 1: -1 ];
(void) sizeof(type_must_be_complete);
delete t;

class Holder
class Impl;
my_scoped_ptr<Impl> _mptr;


class Holder::Impl {};
Holder::Holder() : _mptr(new Impl) {}
Holder::~Holder() {}

How does the non-inline destructor of class Holder suddenly make Impl complete?
Why is the default destructor not sufficient to make the class complete? Why shared_ptr works perfectly well without the need of the destructor?


It's all about the point of instantiation of my_scoped_ptr<Impl>::~my_scoped_ptr.

When you don't provide a user-defined destructor, the default one is defined as soon as the definition of class Holder is processed - basically, it's equivalent to defining the destructor in-class:

class Holder {
  // ... 
  ~Holder() {}

This destructor needs to destroy _mptr member, so ~my_scoped_ptr is also instantiated at this point, while Impl is still incomplete.

When you explicitly declare the destructor in the header, and define in .cpp file, the instantiation of ~my_scoped_ptr happens at the point of that definition - and by that time, Impl is complete.

std::shared_ptr works around this by capturing the deleter at run-time, in its constructor, at the point where it's handed the raw pointer for the first time, and storing it in the control block. You can even assign std::shared_ptr<Derived> to std::shared_ptr<Base>, and the latter will eventually call the correct destructor, even if non-virtual. std::shared_ptr can pull this trick off because it needs to allocate extra storage (for the reference count, among other things) anyway, so it's already somewhat heavyweight. std::unique_ptr on the other hand exhibits the same issue as your my_scoped_ptr, for all the same reasons.