Search code examples
c++shared-ptrsmart-pointersunique-ptr

Why is shared_ptr<void> legal, while unique_ptr<void> is ill-formed?


The question really fits in the title: I am curious to know what is the technical reason for this difference, but also the rationale ?

std::shared_ptr<void> sharedToVoid; // legal;
std::unique_ptr<void> uniqueToVoid; // ill-formed;

Solution

  • It is because std::shared_ptr implements type-erasure, while std::unique_ptr does not.


    Since std::shared_ptr implements type-erasure, it also supports another interesting property, viz. it does not need the type of the deleter as template type argument to the class template. Look at their declarations:

    template<class T,class Deleter = std::default_delete<T> > 
    class unique_ptr;
    

    which has Deleter as type parameter, while

    template<class T> 
    class shared_ptr;
    

    does not have it.

    So, why does shared_ptr implement type-erasure?

    Well, it does so, because it has to support reference-counting, and to support this, it has to allocate memory from heap and since it has to allocate memory anyway, it goes one step further and implements type-erasure — which needs heap allocation too. So basically it is just being opportunistic!

    Because of type-erasure, std::shared_ptr is able to support two things:

    • It can store objects of any type as void*, yet it is still able to delete the objects on destruction properly by correctly invoking their destructor.
    • The type of deleter is not passed as type argument to the class template, which means a little bit freedom without compromising type-safety.

    Alright. That is all about how std::shared_ptr works.

    Now the question is, can std::unique_ptr store objects as void*? Well, the answer is, yes — provided you pass a suitable deleter as argument. Here is one such demonstration:

    int main()
    {
        auto deleter = [](void const * data ) {
            int const * p = static_cast<int const*>(data);
            std::cout << *p << " located at " << p <<  " is being deleted";
            delete p;
        };
        
        std::unique_ptr<void, decltype(deleter)> p(new int(959), deleter);
        
    } //p will be deleted here, both p ;-)
    

    Output (online demo):

    959 located at 0x18aec20 is being deleted
    

    You asked a very interesting question in the comment:

    In my case I will need a type erasing deleter, but it seems possible as well (at the cost of some heap allocation). Basically, does this mean there is actually a niche spot for a 3rd type of smart pointer: an exclusive ownership smart pointer with type erasure.

    to which @Steve Jessop suggested the following solution,

    I've never actually tried this, but maybe you could achieve that by using an appropriate std::function as the deleter type with unique_ptr? Supposing that actually works then you're done, exclusive ownership and a type-erased deleter.

    Following this suggestion, I implemented this (though it does not make use of std::function as it does not seem necessary):

    using unique_void_ptr = std::unique_ptr<void, void(*)(void const*)>;
    
    template<typename T>
    auto unique_void(T * ptr) -> unique_void_ptr
    {
        return unique_void_ptr(ptr, [](void const * data) {
             T const * p = static_cast<T const*>(data);
             std::cout << "{" << *p << "} located at [" << p <<  "] is being deleted.\n";
             delete p;
        });
    }
    
    int main()
    {
        auto p1 = unique_void(new int(959));
        auto p2 = unique_void(new double(595.5));
        auto p3 = unique_void(new std::string("Hello World"));
    }  
    

    Output (online demo):

    {Hello World} located at [0x2364c60] is being deleted.
    {595.5} located at [0x2364c40] is being deleted.
    {959} located at [0x2364c20] is being deleted.
    

    To avoid the naked new, you can then:

    template<typename T, typename... Args>
    auto make_unique_void(Args&&... args)
    {
         return unique_void(new T(std::forward<Args>(args)...));
    }
    

    in place of std::make_unique