6

I would like to understand why unique_ptr destructors require the type to be complete upon destruction while that isn't the case with shared_ptr. This blog from Howard Hinnant briefly mentions it has to do with static vs. dynamic deleters. I'm looking for a more detailed explanation of why that might be the case (it may be compiler implementation specific in which case an example would be helpful). With dynamic deleters, does it restrict the destructor from being inlined?

cigien
  • 57,834
  • 11
  • 73
  • 112
user3882729
  • 1,339
  • 8
  • 11
  • `shared_ptr` captures a deleter at the time of construction, and stores it in the control block. E.g. `shared_ptr ptr(new Derived);` works correctly, and eventually calls the correct destructor `~Derived`, even if `Base` doesn't have a virtual destructor (because the constructor essentially saves `[dervied_ptr]() { delete dervied_ptr; }`, and the destructor calls the same). `std::unique_ptr` doesn't do anything like that; its destructor just does `delete p` on the raw pointer `p` of type `T*` that this `unique_ptr` stores. – Igor Tandetnik Apr 11 '22 at 00:27
  • @IgorTandetnik I've posted as a follow-up to the answer below, but what are the implications of above when it comes to using ```shared_ptr``` vs. ```unique_ptr``` when it comes to ```pImpl``` idiom. Specifically why isn't there a requirement to define the destructor of the wrapping class in the ```cpp``` file, it seems to have to do with inlining of destructor with ```unique_ptr``` that's not possible with ```shared_ptr``` but it's not obvious to me as to why. – user3882729 Apr 11 '22 at 01:11
  • 1
    `unique_ptr` needs `T` to be a complete type at the point where `~unique_ptr` destructor is called. `shared_ptr` needs `T` (or rather, the pointer passed to the constructor, which may e.g. be a class derived from `T`) to be complete at the point where its constructor is called. If you don't put the destructor of the wrapper class into the `cpp` file, it will be implicitly defined in the `h` file, and that implicit definition is going to call the destructor of the smart pointer, at the time `pImpl` class is incomplete. That's a problem for `unique_ptr`, not a problem for `shared_ptr` – Igor Tandetnik Apr 11 '22 at 02:10

2 Answers2

6

Howard Hinnant was simplifying. What he precisely meant was if you use the default deleter for std::unique_ptr, you need a complete type. For the default deleter, it simply calls delete for you.

The gist of static and dynamic deleters is

class A;

void static_delete(A* p)
{
    delete p;
}

void (*dynamic_delete)(A*) = /* somehow */;

int main()
{
    A* p = /* somehow */;
    static_delete(p);  // undefined behaviour
    dynamic_delete(p); // maybe not
}

As long asdynamic_delete points to a function that's defined where A is also defined, you have well-defined behaviour.

To prevent undefined behaviour, the default deleter checks for a complete type, which may be implemented as

void default_delete(A* p)
{
    static_assert(sizeof(A) > 0);
    delete p;
}
Passer By
  • 19,325
  • 6
  • 49
  • 96
  • In the context of destructors of ```shared_ptr``` vs. ```unique_ptr``` when it applies to the ```pImpl``` idiom, the destructor of the wrapping class has to be defined in the cpp file but there is no such restriction if ```shared_ptr``` is used instead. Why might that be the case, i.e. what guarantees that the dynamic deleter will be defined after the implementation class? – user3882729 Apr 11 '22 at 01:03
  • @user3882729 If it isn't, you get a compile error when you try to create `dynamic_delete`. But that's a different issue. The point is, you don't need a complete type in `main`. – Passer By Apr 11 '22 at 01:28
1

I would like to understand why unique_ptr destructors require the type to be complete upon destruction while that isn't the case with shared_ptr

I believe that is as simple as that default destructor needs to know the size of data members. For unique_ptr the deleter is part of the object representation, whilst for shared_ptr the actual deleter is not that relevant, because it's stored in the control block, no matter what kind of deleter you use

The Dreams Wind
  • 8,416
  • 2
  • 19
  • 49