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

2

124

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;
Hyo answered 2/9, 2016 at 9:24 Comment(0)
O
148

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

Orchestra answered 2/9, 2016 at 9:27 Comment(7)
Good answer, +1. But you might make it even better by explicitly mentioning that a std::unique_ptr<void, D> is still possible by providing a suitable D.Willow
@Angrew: Nice one, you found the real underlying question that was not written in my question ;)Hyo
@Nawaz: Thank you. 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 ?Hyo
@AdN: 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.Superorder
Um... I don't see the need for std::function in the second part of your message or in any additional type erasure provided by std::function. Just pass a pointer to an instantiation or your deleter<T> as a deleter and that's all "type erasure" you will ever need in this example. No need to involve std::function at all. See: coliru.stacked-crooked.com/a/3b2c04aadc638a02 We should probably as @Steve Jessop about what he meant by his reference to std::function.Delegate
@AnT: You're right. std::function is unnecessary, as T is known inside the pointer having the same pointer-type for all T. Changed the example. Thanks for pointing out. :-)Orchestra
@AnT std::function allows you to type erase stateful deletersMarillin
B
7

One of the rationales is in one of the many use-cases of a shared_ptr - namely as a lifetime indicator or sentinel.

This was mentioned in the original boost documentation:

auto register_callback(std::function<void()> closure, std::shared_ptr<void> pv)
{
    auto closure_target = { closure, std::weak_ptr<void>(pv) };
    ...
    // store the target somewhere, and later....
}

void call_closure(closure_target target)
{
    // test whether target of the closure still exists
    auto lock = target.sentinel.lock();
    if (lock) {
        // if so, call the closure
        target.closure();
    }
}

Where closure_target is something like this:

struct closure_target {
    std::function<void()> closure;
    std::weak_ptr<void> sentinel;
};

The caller would register a callback something like this:

struct active_object : std::enable_shared_from_this<active_object>
{
    void start() {
      event_emitter_.register_callback([this] { this->on_callback(); }, 
                                       shared_from_this());
    }

    void on_callback()
    {
        // this is only ever called if we still exist 
    }
};

because shared_ptr<X> is always convertible to shared_ptr<void>, the event_emitter can now be blissfully unaware of the type of object it is calling back into.

This arrangement releases subscribers to the event emitter of the obligation of handling crossing cases (what if the callback in on a queue, waiting to be actioned while active_object goes away?), and also means that there is no need to synchronise unsubscription. weak_ptr<void>::lock is a synchronised operation.

Buddhi answered 2/9, 2016 at 9:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.