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.
std::shared_ptr is a smart pointer that retains shared ownership of an object through a pointer.
The smart pointer has an internal counter which is decreased each time that a std::shared_ptr , pointing to the same resource, goes out of scope – this technique is called reference counting. When the last shared pointer is destroyed, the counter goes to zero, and the memory is deallocated.
By moving the shared_ptr instead of copying it, we "steal" the atomic reference count and we nullify the other shared_ptr . "stealing" the reference count is not atomic, and it is hundred times faster than copying the shared_ptr (and causing atomic reference increment or decrement).
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 cleanupd(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 thenew
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. :-)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With