If I create a single shared_ptr with a custom deleter and that shared_ptr gets destroyed the custom deleter will get called - inside that deleter's body what will be the value of the shared_ptr's reference count? 1 or 0?
I couldn't find it in the documentation. Is that something guaranteed by the standard?
The standard defines that the initial use_count
is 1. Decrementing has to be atomic and the deleter must only be called once. For multiple threads to achieve this, the decrementing must be an atomic read-modify-write, ensuring that only one thread sees the final use_count
reach zero. If it does, it calls the deleter.
Specifically: If multiple threads destroy their shared_ptrs
at the same time, they will all potentially observe a use_count > 1
before decrementing but only one will observe it reaching 0. The count being greater than 1 before decrementing does not rule out that you're responsible for deleting afterwards. Therefore the deleter cannot possibly be called before decrementing.
Note that a deleter can only observe the use_count
if it owns a weak_ptr
to the same object since by definition it is called once the last shared_ptr
to its own object has been destroyed, so it cannot have a valid reference to one. It cannot possibly be used to revive the object. Attempting to lock the weak_ptr
will result in a default-contructed shared_ptr
which, incidentally, is the only time you can observe use_count
being zero.
Here is a simple example to show this:
#include <iostream>
#include <memory>
struct Foo: std::enable_shared_from_this<Foo>
{};
struct Deleter
{
void operator()(Foo* ptr) const
{
std::weak_ptr<Foo> weak = ptr->weak_from_this();
std::shared_ptr<Foo> revived = weak.lock();
std::cout << "Deleter reviving object? "
<< (revived ? "true" : "false") << '\n';
delete ptr;
}
};
int main()
{
std::shared_ptr<Foo> f { new Foo, Deleter{} };
}
The standard defines that weak_ptr::lock()
creates a valid shared_ptr
if use_count > 0
. Specifically
Returns:
expired() ? shared_ptr<T>() : shared_ptr<T>(*this)
, executed atomically.
with expired()
being defined as
Returns:
use_count() == 0
So if use_count() > 0
could possibly be observed, resurrecting objects would work, causing the deleter to invoke itself recursively in the example above. While I couldn't find any mention to this scenario in the standard, it seems reasonable that implementations avoid this.
While this example here is clearly dumb code, it can easily happen in production code that the deleter or the object's destructor invoke code that has access to a weak_ptr
to the object that is being destroyed.