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;
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;
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.
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:
void*
, yet it is still able to delete the objects on destruction properly by correctly invoking their destructor.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 withunique_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
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 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 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 std::function
allows you to type erase stateful deleters –
Marillin 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.
© 2022 - 2024 — McMap. All rights reserved.
std::unique_ptr<void, D>
is still possible by providing a suitableD
. – Willow