2
votes

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
{
private:
    T *t;
public:
    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
{
public:
    Holder();
    ~Holder();
private:
    class Impl;
    my_scoped_ptr<Impl> _mptr;
};

.cpp

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?

2
About shared_ptr, examine the implementation to see the amazing tricks they pull to work around undefined behavior.Alan Baljeu

2 Answers

5
votes

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.

0
votes

It isn't complete in the header file.

~Holder(); is declaring an external function.

declaring ~Holder() = default; is equivalent to declaring ~Holder() {}, which is to say that it's providing a definition of the declared destructor. It cannot do this because the inner Impl class has only been declared at this point, not defined.