Why does unique_ptr have the deleter as a type parameter while shared_ptr doesn't?
Asked Answered
B

3

71

The std::unique_ptr template has two parameters: the type of the pointee, and the type of the deleter. This second parameter has a default value, so you usually just write something like std::unique_ptr<int>.

The std::shared_ptr template has only one parameter though: the type of the pointee. But you can use a custom deleter with this one too, even though the deleter type is not in the class template. The usual implementation uses type erasure techniques to do this.

Is there a reason the same idea was not used for std::unique_ptr?

Babby answered 26/7, 2011 at 11:51 Comment(0)
F
44

Part of the reason is that shared_ptr needs an explicit control block anyway for the ref count and sticking a deleter in isn't that big a deal on top. unique_ptr however doesn't require any additional overhead, and adding it would be unpopular- it's supposed to be a zero-overhead class. unique_ptr is supposed to be static.

You can always add your own type erasure on top if you want that behaviour- for example, you can have unique_ptr<T, std::function<void(T*)>>, something that I have done in the past.

Factorial answered 26/7, 2011 at 11:56 Comment(5)
+1. Type erasure isn't a "good thing", it's more like a necessary evil. If you can get away with a simpler solution, then you should. unique_ptr is a much simpler and lighter-weight smart pointer than shared_ptr.Antananarivo
@DeadMG: I don't get the "unique_ptr is supposed to be static" part. What does that mean -- specifically, the usage of "static"?Abdella
@Jon: It means that it should not resolve the functionality used dynamically- the compiler should statically resolve all calls involved. This enables it to compete against your momma's best home-rolled C code w.r.t. performance in all respects.Factorial
@DeadMG: So, "static" in this context is a weaker version of "zero-overhead" (which I read as "static and also no bigger mem footprint"). That clears it up, thanks.Abdella
Wouldn't it be possible to have the best of both worlds? For certain deleter types (including the default), there is only one possible value for the deleter, and therefore no memory overhead. But, for certain deleter types, the unique_ptr would be expanded to include the value of the deleter. I guess this might be complex, and might have required a different design of the class.Kronos
L
4

From C++ Primer (5th Edition), Chapter 16.1.6 - Efficiency and Flexibility

The obvious difference between shared_ptr and unique_ptr is the strategy they use in managing the pointer they hold—one class gives us shared ownership; the other owns the pointer that it holds. This difference is essential to what these classes do.

These classes also differ in how they let users override their default deleter. We can easily override the deleter of a shared_ptr by passing a callable object when we create or reset the pointer. In contrast, the type of the deleter is part of the type of a unique_ptr object. Users must supply that type as an explicit template argument when they define a unique_ptr. As a result, it is more complicated for users of unique_ptr to provide their own deleter.

By binding the deleter at compile time, unique_ptr avoids the run-time cost of an indirect call to its deleter. By binding the deleter at run time, shared_ptr makes it easier for users to override the deleter.

Lusaka answered 27/1, 2022 at 13:45 Comment(0)
C
2

Another reason, in addition to the one pointed out by DeadMG, would be that it's possible to write

std::unique_ptr<int[]> a(new int[100]);

and ~unique_ptr will call the correct version of delete (via default_delete<_Tp[]>) thanks to specializing for both T and T[].

Counterblast answered 26/7, 2011 at 13:21 Comment(6)
I don't see how that changes much. You could specialise for T[] without the deleter parameter.Babby
Yes, it would be possible, but less elegant and less flexible, as it would be hardcoded inside unique_ptr, not the deleter. Note that you could plug in a foo_delete which could do something completely different, too. Having a deleter that manually runs the destructor and then marks objects for later reclamation by a GC would be a fun thing, for example. The smart pointer wouldn't need to know at all.Counterblast
Just as shared_ptr doesn't need to know. You could do the same type erasure tricks when specializing for T[], no?Babby
Not sure... maybe, maybe not. The same line of code using shared_ptr fails to compile, which shows that shared_ptr apparently doesn't specialize. Assuming that the standard library writers don't randomly include/exclude key functionality which is easy to incorporate on a how-do-you-feel-today base, it seems that there must be a considerable, non-obvious hindrance somewhere.Counterblast
@Damon: T[] specialization is not at all key functionality. I don't even know why it's in unique_ptr. We already have std::vector<T> for this purpose.Factorial
@DeadMG: Are you suggesting that unique_ptr should not have a specialization for T[]? I think the alternative would be very undesirable. As far as vector<T> goes, it is not a drop in replacement for unique_ptr<T[]> which I can see some (albeit not common) uses for, the first of which is legacy code reasons.Maskanonge

© 2022 - 2024 — McMap. All rights reserved.