Shouldn't `std::shared_ptr` use `std::default_delete` by default?
Asked Answered
A

1

6

std::default_delete can be specialized to allow std::unique_ptrs to painlessly manage types which have to be destroyed by calling some custom destroy-function instead of using delete p;.

There are basically two ways to make sure an object is managed by a std::shared_ptr in C++:

  1. Create it managed by a shared-pointer, using std::make_shared or std::allocate_shared. This is the preferred way, as it coalesces both memory-blocks needed (payload and reference-counts) into one. Though iff there are only std::weak_ptrs left, the need for the reference-counts will by necessity still pin down the memory for the payload too.

  2. Assign management to a shared-pointer afterwards, using a constructor or .reset().

The second case, when not providing a custom deleter is interesting:

Specifically, it is defined to use its own deleter of unspecified type which uses delete [] p; or delete p; respectively, depending on the std::shared_ptr being instantiated for an Array or not.

Quote from n4659 (~C++17):

template<class Y> explicit shared_ptr(Y* p);

4 Requires: Y shall be a complete type. The expression delete[] p, when T is an array type, or delete p, when T is not an array type, shall have well-defined behavior, and shall not throw exceptions.
5 Effects: When T is not an Array type, constructs a shared_ptr object that owns the pointer p. Otherwise, constructs a shared_ptr that owns p and a deleter of an unspecified type that calls delete[] p. When T is not an array type, enables shared_from_this with p. If an exception is thrown, delete p is called when T is not an array type, delete[] p otherwise.
6 Postconditions: use_count() == 1 && get() == p.
[…]

template<class Y> void reset(Y* p);

3 Effects: Equivalent to shared_ptr(p).swap(*this).

My questions are:

  1. Is there a, preferably good, reason that it is not specified to use std::default_delete instead?
  2. Would any valid (and potentially useful?) code be broken by that change?
  3. Is there already a proposal to do so?
Appulse answered 10/7, 2018 at 0:12 Comment(4)
Well, your specialization has to "meet the standard library requirements for the original template", and part of said requirements is that it calls delete (or delete[]). I don't see how you can legally specialize it to do something else.Judejudea
Does the default delete still work when the control block is created with the same memory block as the instance. (Weak pointers require a different moment of destruction)Gyasi
@Judejudea The specialization deletes the object, in the only proper way. The base template would have been wrong, so no problem.Appulse
@JVApen: There is no separate deleter used if std::make_shared or std::allocate_shared coalesce both allocations. That's only background for the question.Appulse
M
4

Is there a, preferably good, reason that it is not specified to use std::default_delete instead?

Because it wouldn't do what you want. See, just because you can specialize something doesn't mean you can hijack it. The standard says ([namespace.std]):

A program may add a template specialization for any standard library template to namespace std only if the declaration depends on a user-defined type and the specialization meets the standard library requirements for the original template and is not explicitly prohibited.

The standard library requirement for std::default_delete<T>::operator()(T* ptr)'s behavior is that it "Calls delete on ptr." So your specialization of it must do the same.

As such, there should be no difference between having shared_ptr perform delete ptr; and having shared_ptr invoke default_delete<T>{}(ptr).

This is why unique_ptr takes a deleter type, rather than relying on you to specialize it.


From the comments:

The specialization deletes the object, in the only proper way.

But that's not what the requirement says. It says "Calls delete on ptr." It does not say something more ambiguous like "ends the lifetime of the object pointed to by ptr" or "destroys the object referenced by ptr". It gives explicit code that must happen.

And your specialization has to follow through.

If you remain unconvinced, the paper P0722R1 says this:

Note that the standard requires specializations of default_delete<T> to have the same effect as calling delete p;,

So clearly, the authors agree that specializing default_delete is not a mechanism for adding your own behavior.

So the premise of your question is invalid.


However, let's pretend for a moment that your question were valid, that such a specialization would work. Valid or not, specializing default_delete to customize deleter behavior is not the intended method of doing so. If it were the intent, you wouldn't need a deleter object for unique_ptr at all. At most, you would just need a parameter that tells you what the pointer type is, which would default to T*.

So that's a good reason not to do this.

Mullen answered 10/7, 2018 at 2:40 Comment(2)
Regarding the last point of your answer, pretending that specializing default_delete to call a custom destroy instead of delete is valid, there would still be a benefit to having a deleter for unique_ptr when you want to use different deleters for the same T. That might not be a very good use-case (strong types over T might be a good idea), but it does exist.Cottonweed
So, to summarize: No to all three questions, but any useful specialialization of std::default_delete is invalid so it doesn't really matter?Appulse

© 2022 - 2024 — McMap. All rights reserved.