40

In C++11 std::shared_ptr has four constructors which can be passed deleter objects d of type D. The signatures of these constructors are the following:

template<class Y, class D> shared_ptr(Y * p, D d);
template<class Y, class D, class A> shared_ptr(Y * p, D d, A a);
template <class D> shared_ptr(nullptr_t p, D d);
template <class D, class A> shared_ptr(nullptr_t p, D d, A a);

The standard requires in [util.smartptr.shared.const] type D to be CopyConstructible. Why is this needed? If shared_ptr makes copies of d then which of these deleters might get called? Wouldn't it possible for a shared_ptr only to keep a single deleter around? What does it mean for a shared_ptr to own a deleter if d can be copied?

What is the rationale behind the CopyConstructible requirement?

PS: This requirement might complicate writing deleters for shared_ptr. unique_ptr seems to have much better requirements for its deleter.

jotik
  • 17,044
  • 13
  • 58
  • 123
  • If `d` is not copyConstructible, then how would the *shared_ptr's constructor* will store it as internal data to be used later? – Nawaz Apr 20 '16 at 13:05
  • 7
    @Nawaz: By movement. – Nicol Bolas Apr 20 '16 at 13:06
  • I thought *move* is just an efficient version of *copy*. – Nawaz Apr 20 '16 at 13:07
  • 3
    @Nawaz: Conceptually, yes. But a type can be MoveConstructible and not CopyConstructible. Like `unique_ptr`. His question is essentially why `shared_ptr` forbids the use of a move-only Deleter type. – Nicol Bolas Apr 20 '16 at 13:07
  • easier to implement thread safety? e.g. majority of members are copied so only the minority shared members need to be handled? – user3528438 Apr 20 '16 at 13:19
  • I have to wonder if they did this so that implementations could copy the deleter into a weak_ptr. What makes we think this is the description of the return from `get_deleter`: *Returns: If p owns a deleter d of type cv-unqualified D, returns &d; otherwise returns 0. The returned pointer remains valid as long as there exists a shared_ptr instance that owns d. [ Note: It is unspecified whether the pointer remains valid longer than that. This can happen if the implementation doesn’t destroy the deleter until all weak_ptr instances that share ownership with p have been destroyed. —end note ]* – NathanOliver Apr 20 '16 at 13:31
  • @Nawaz I would not think it is wise to say that move is a version of copy; with a copy, the source is still available. With a move, the source contents are no longer there (though the variable, if available, is still there, with undefined contents). – johnbakers Apr 20 '16 at 14:10
  • @NathanOliver get_deleter need you offer the type of deleter, so I think nullptr will returned if the type is wrong. And deleter for shared_ptr is also a part of state, weak_ptr should also hold it. – linux40 Apr 20 '16 at 16:12
  • 7
    I suspect that the answer is that it was designed at a time when there was no move semantics. Right now I can't think of anything fundamentally problematic with a move-only deleter. – T.C. Apr 20 '16 at 18:56
  • @T.C. As noted by Maxim in the comments of his answer, `shared_ptr` was first introduced in TR1. When it was moved from `std::tr1` to `std` for C++11, they added some support for move semantics. It is possible that the CopyConstructible requirement was left due to oversight. – jotik Apr 27 '16 at 12:50
  • 4
    Maybe caused by that deleter can be held by a `std::function` only if it is CopyConstructible? – user1887915 May 11 '16 at 17:37

3 Answers3

23

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. :-)

AndyG
  • 39,700
  • 8
  • 109
  • 143
3

The difference between deleters in std::shared_ptr and std::unique_ptr is that shared_ptr deleter is type-erased, while in unique_ptr deleter type is part of the template.

Here is Stephan T. Lavavej explaining how type erasure leads to CopyConstructible requirement in std::function.

As for the reason behind this difference in pointer types, it has been addressed on SO several times, e.g. here.

A quote of what S.T.L. said:

Very surprising "gotcha" I would say is that the std::function requires CopyConstructible function objects, and this is kind of unusual in the STL.

Usually the STL is lazy in the sense that it doesn't need things up front: if I have something like a std::list of type T, T does not need to be less-than-comparable; only if you call the member function list<T>::sort then it actually does need to be less-than-comparable.

The core language rule that powers this is that the definitions of member functions of a class template are not instantiated until they're actually needed and the bodies don't exist in some sense until you actually call it.

This is usually cool - this means you only pay for what you need, but std::function is special because of type erasure, because when you construct the std::function from some callable object F it needs to generate everything you could ever need from that object F because it's going to erase its type. It requires all the operations that it could possibly ever need regardless of if they're used.

So if you construct a std::function from some callable object F, F is absolutely required at compile-time to be CopyConstructible. This is true even though F will be moved into the std::function, so even if you give it an r-value and even if you never copy std::functions anywhere in your program, F is still required to be CopyConstructible.

You'll get a compiler error saying so - maybe horrible, maybe nice - depending on what you get.

It just cannot store movable only function objects. This is a design limitation caused in part by the fact that std::function dates back to boost/TR1, before r-value references, and in some sense it can never be fixed with std::function's interface as it stands.

Alternatives are being investigated, maybe we can have a different "movable function", so we will probably get some sort of type-erased wrapper that can store movable only function in the future, but std::function as it stands in c++17 right now cannot do that, so just be aware.

Community
  • 1
  • 1
Ap31
  • 3,244
  • 1
  • 18
  • 25
  • @jotik I just added the whole quote that I extracted from YouTube closed captions, hopefully this is OK – Ap31 Sep 29 '16 at 12:22
-2

Because shared_ptr are meant to be copied, and any of those copy could have to delete the object, so they must all have access to a deleter. Keeping only one deleter would require, well, refcounting the deleter itself. If you really want that to happen, you could use a nested std::shared_ptr as the deleter, but that sounds a bit overkill.

mefyl
  • 264
  • 1
  • 6
  • 9
    But the deleter already is reference counted. Most implementations stick it in the control block with the pointer to be deleted. Copying a `shared_ptr` will not copy the deleter. – Nicol Bolas Apr 20 '16 at 13:04
  • Is that certain (i.e. specified in the standard) ? In that case you would still need to copy in once to the refcounted chunk, but I stand corrected. – mefyl Apr 20 '16 at 13:07
  • 2
    Not sure why you're being downvoted. It would be perfectly reasonable for an (albeit, naïve) implementation to do this. – gmbeard Apr 22 '16 at 05:37
  • No offense taken, they have a perfect point: it can be designed so only one copy is needed, and it could be a move. So while my answer is a potential explanation as to why it has to be copiable, it's quite inaccurate. – mefyl Apr 22 '16 at 12:28
  • @NicolBolas, still, standard doesn't require deleter to be reference-counted. Which makes perfect sense why deleter has to be copy-constructible. – Andrei R. Apr 27 '16 at 12:07
  • @AndreiR.: "*standard doesn't require deleter to be reference-counted.*" No, it doesn't. But the only way to implement `shared_ptr` where the deleter is not stored with the control block is to store it in a type-erased value type with every `shared_ptr`. And `weak_ptr`, for that matter. Why would you ever implement it like that? The main point is that Deleters are not required to be copy-constructible just to support stupid implementations of `shared_ptr`. – Nicol Bolas Apr 27 '16 at 14:10