5

std::shared_ptr has a nifty templated constructor that automagically creates the right deleter for its given type (constructor #2 in that link).

Until just now, I (erroneously) thought std::unique_ptr had a similar constructor, but when I ran the following code:

#include <memory>
#include <iostream>

// Notice nothing is virtual
struct Foo
{
    ~Foo() { std::cout << "Foo\n"; }
};

struct Bar : public Foo
{
    ~Bar() { std::cout << "Bar\n"; }
};

int main()
{
    {
        std::cout << "shared_ptr:\n";
        std::shared_ptr<Foo> p(new Bar()); // prints Bar Foo
    }

    {
        std::cout << "unique_ptr:\n";
        std::unique_ptr<Foo> p(new Bar()); // prints Foo
    }
}

I was surprised to learn that unique_ptr doesn't call Bar's destructor.

What's a clean, simple, and correct way to create a unique_ptr that has the correct deleter for its given pointer? Especially if I want to store a whole list of these (i.e. std::vector<std::unique_ptr<Foo>>), which means that they all must have a heterogeneous type?

(pardon the poor title; feel free to suggest a better one)

Cornstalks
  • 37,137
  • 18
  • 79
  • 144
  • `unique_ptr`'s deleter is in its type. If you make a vector of `std::unique_ptr`, then they are all going to use `std::default_delete`. – T.C. Oct 05 '14 at 05:01
  • http://stackoverflow.com/questions/6829576/why-does-unique-ptr-have-the-deleter-as-a-type-parameter-while-shared-ptr-doesn – Bill Lynch Oct 05 '14 at 05:05
  • 3
    I think the problem is with your class, not `std::unique_ptr`. Your destructor needs to be virtual. – Galik Oct 05 '14 at 05:55

2 Answers2

7

You should make the destructor of Foo virtual. That is good practice regardless of whether you use unique_ptr or not. That will also take care the problem that you are dealing with.

R Sahu
  • 204,454
  • 14
  • 159
  • 270
5

Here's one way:

{
    std::cout << "unique_ptr<Bar, void(void*)>:\n";
    std::unique_ptr<Foo, void(*)(void*)> p(
        new Bar(), [](void*p) -> void { delete static_cast<Bar*>( p ); }
        ); // prints Bar Foo
}

A main problem with this approach is that unique_ptr supports conversion to logical "pointer to base class", but that the standard does not guarantee that conversion to void* will then yield the same address. In practice that's only a problem if the base class is non-polymorphic while the derived class is polymorphic, introducing a vtable ptr and thus possibly changing the memory layout a bit. But in that possible-but-not-likely situation the cast back in the deleter would yield an incorrect pointer value, and bang.

So, the above is not formally safe with respect to such conversions.


To do roughly the same as a shared_ptr does (shared_ptr supports conversions to logical pointer-to-base), you would need to store also the original void* pointer, along with the deleter.


In general, when you control the the topmost base class, make its destructor virtual.

That takes care of everything.

Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • For the problematic scenario, how about double `static_cast` - cast the `void *` to `Foo *` first and then to `Bar *`? – T.C. Oct 05 '14 at 09:00
  • @T.C. The problem is that the deleter doesn't know the pointer type that was passed to the `void*`. Depending on the type it can point to different addresses. But now that you set me thinking about it, the problem is just that `void*` discards too much information. I think all would be OK with `Foo*` argument type instead, then casted down to `Bar*` in the deleter. Or, is there a problem with that? It's a bit late in the day for me. – Cheers and hth. - Alf Oct 05 '14 at 09:08
  • The pointer passed to the deleter is not always useful, even if the types are OK. Sometimes static_cast can be impossible, for example with virtual inheritance (http://stackoverflow.com/questions/7484913/why-cant-static-cast-be-used-to-down-cast-when-virtual-inheritance-is-involved). I think the only really robust solution is for the deleter to remember the original pointer value (as was returned from `new`). This means the deleter formally ignores its argument, and therefore it may as well be `void*`. Finally, once you pay this memory 'price', you can also add a `deep_copy` at no extra cost. – Aaron McDaid Jul 29 '15 at 09:14