Why Does Unique_Ptr Take Two Template Parameters When Shared_Ptr Only Takes One

Why does unique_ptr take two template parameters when shared_ptr only takes one?

If you provide the deleter as template argument (as in unique_ptr) it is part of the type and you don't need to store anything additional in the objects of this type.
If deleter is passed as constructor's argument (as in shared_ptr) you need to store it in the object. This is the cost of additional flexibility, since you can use different deleters for the objects of the same type.

I guess this is the reason: unique_ptr is supposed to be very lightweight object with zero overhead. Storing deleters with each unique_ptr could double their size. Because of that people would use good old raw pointers instead, which would be wrong.

On the other hand, shared_ptr is not that lightweight, since it needs to store reference count, so storing a custom deleter too looks like good trade off.

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

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.

Combing two factory methods returning unique_ptr and shared_ptr into one in C++?

You could use SFINAE, but then I don't really see the point to have this inside a function anymore. It's pretty redundant.

#include <memory>
#include <type_traits>

template <class T, class... Args>
typename std::enable_if<
std::is_same<T, std::shared_ptr<typename T::element_type>>::value, T>::type
createObject(Args&&... args) {
// running some code
return std::make_shared<typename T::element_type>(std::forward<Args>(args)...);
}
template <class T, class... Args>
typename std::enable_if<
std::is_same<T, std::unique_ptr<typename T::element_type>>::value, T>::type
createObject(Args&&... args) {
// running some code
return std::make_unique<typename T::element_type>(std::forward<Args>(args)...);
}

int main() {
auto s = createObject<std::shared_ptr<int>>(1);
auto u = createObject<std::unique_ptr<int>>(1);
}

A little bit more compact but essentially the same idea with a scoped enum

#include <memory>

enum class ptr_t { shared, unique };

template <ptr_t P, class T, class... Args>
typename std::enable_if<P == ptr_t::shared, std::shared_ptr<T>>::type
createObject(Args&&... args) {
// running some code
return std::make_shared<T>(std::forward<Args>(args)...);
}
template <ptr_t P, class T, class... Args>
typename std::enable_if<P == ptr_t::unique, std::unique_ptr<T>>::type
createObject(Args&&... args) {
// running some code
return std::make_unique<T>(std::forward<Args>(args)...);
}

int main() {
auto s = createObject<ptr_t::shared, int>(1);
auto u = createObject<ptr_t::unique, int>(1);
}

In C++17 you of course use if constexpr in both cases rather than SFINAE.

#include <memory>

enum class ptr_t { shared, unique };

template <ptr_t P, class T, class... Args>
decltype(auto) createObject(Args &&... args) {
// running some code
if constexpr (P == ptr_t::shared) {
return std::make_shared<T>(std::forward<Args>(args)...);
} else if (P == ptr_t::unique) {
return std::make_unique<T>(std::forward<Args>(args)...);
}
}

Deleter type in unique_ptr vs. shared_ptr

Here you can find the original proposal for smart pointers: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1450.html

It answers your question quite precisely:

Since the deleter is not part of the type, changing the allocation strategy does not break source or binary compatibility, and does not require a client recompilation.

This is also useful because gives the clients of std::shared_ptr some more flexibility, for example shared_ptr instances with different deleters can be stored in the same container.

Also, because the shared_ptr implementations needs a shared memory block anyhow (for storing the reference count) and because there alreay has to be some overhead compared to raw pointers, adding a type-erased deleter is not much of a big deal here.

unique_ptr on the other hand are inteded to have no overhead at all and every instance has to embed its deleter, so making it a part of the type is the natural thing to do.

Differences between unique_ptr and shared_ptr

Both of these classes are smart pointers, which means that they automatically (in most cases) will deallocate the object that they point at when that object can no longer be referenced. The difference between the two is how many different pointers of each type can refer to a resource.

When using unique_ptr, there can be at most one unique_ptr pointing at any one resource. When that unique_ptr is destroyed, the resource is automatically reclaimed. Because there can only be one unique_ptr to any resource, any attempt to make a copy of a unique_ptr will cause a compile-time error. For example, this code is illegal:

unique_ptr<T> myPtr(new T);       // Okay
unique_ptr<T> myOtherPtr = myPtr; // Error: Can't copy unique_ptr

However, unique_ptr can be moved using the new move semantics:

unique_ptr<T> myPtr(new T);                  // Okay
unique_ptr<T> myOtherPtr = std::move(myPtr); // Okay, resource now stored in myOtherPtr

Similarly, you can do something like this:

unique_ptr<T> MyFunction() {
unique_ptr<T> myPtr(/* ... */);

/* ... */

return myPtr;
}

This idiom means "I'm returning a managed resource to you. If you don't explicitly capture the return value, then the resource will be cleaned up. If you do, then you now have exclusive ownership of that resource." In this way, you can think of unique_ptr as a safer, better replacement for auto_ptr.

shared_ptr, on the other hand, allows for multiple pointers to point at a given resource. When the very last shared_ptr to a resource is destroyed, the resource will be deallocated. For example, this code is perfectly legal:

shared_ptr<T> myPtr(new T);       // Okay
shared_ptr<T> myOtherPtr = myPtr; // Sure! Now have two pointers to the resource.

Internally, shared_ptr uses reference counting to track how many pointers refer to a resource, so you need to be careful not to introduce any reference cycles.

In short:

  1. Use unique_ptr when you want a single pointer to an object that will be reclaimed when that single pointer is destroyed.
  2. Use shared_ptr when you want multiple pointers to the same resource.

How can unique_ptr have no overhead if it needs to store the deleter?

std::unique_ptr<T> is quite likely to be zero-overhead (with any sane standard-library implementation). std::unique_ptr<T, D>, for an arbitrary D, is not in general zero-overhead.

The reason is simple: Empty-Base Optimisation can be used to eliminate storage of the deleter in case it's an empty (and thus stateless) type (such as std::default_delete instantiations).

How to feed a shared_ptr to a template function that shall add various data types to a vector?

You can't "move" the thing inside a shared_ptr into an instance of a unique_ptr. If you could, all kinds of problems would occur as a result of two different smart pointers trying to delete the thing inside it when they get released.

Instead, just delcare your vector as :

std::vector<std::shared_ptr<segment>> v3;

And then, you really don't need move semantics after that:

template<typename T1, typename T2>
inline void add_segment(T1 & v, T2& line_or_circle)
{
v.emplace_back(line_or_circle);
}

Why do shared_ptr deleters have to be CopyConstructible?

This question was perplexing enough that I emailed Peter Dimov (implementer of boost::shared_ptr and involved in standardization of std::shared_ptr)

Here's the gist of what he said (reprinted with his permission):

My guess is that the Deleter had to be CopyConstructible really only as a
relic of C++03 where move semantics didn’t exist.


Your guess is correct. When shared_ptr was specified rvalue references
didn't exist yet. Nowadays we should be able to get by with requiring
nothrow move-constructible.

There is one subtlety in that when

pi_ = new sp_counted_impl_pd<P, D>(p, d);

throws, d must be left intact for the cleanup d(p) to work, but I
think that this would not be a problem (although I haven't actually
tried to make the implementation move-friendly).

[...]

I think that there will be no problem for the
implementation to define it so that when the new throws, d will be left
in its original state.

If we go further and allow D to have a throwing move constructor, things get
more complicated. But we won't. :-)



Related Topics



Leave a reply



Submit