Home > OS >  Why does the impl of shared_ptr ref count hold a pointer to the actual pointed type?
Why does the impl of shared_ptr ref count hold a pointer to the actual pointed type?

Time:08-03

This was motivated by an interview question:

shared_ptr<void> p(new Foo());

Will the destructor of Foo get called once p goes out of scope?

It turns out it does, I had to look at the implementation of shared_ptr in GCC 1, and find out that apparently the control block holds a pointer to the actual type (Foo) and a pointer to the destructor that gets invoked when the ref count reaches 0.

1: Sorry I am on my phone I cannot copy the link to the impl.

But I am still wondering: why? Why is it needed? Is there anything I am missing from the standard?

On the other hand, the line above doesn't compile with unique_ptr because obviously there's no ref count in that case.

CodePudding user response:

A std::shared_ptr<T> instance itself must keep track of the pointer to return when .get() is called. This is always of type T*, except when T is an array, in which case it is of type std::remove_extent_t<T>* (for example, std::shared_ptr<int[]>::get() returns int*).

Also, when a std::shared_ptr<T> is destroyed, it has to check whether it is the last std::shared_ptr instance referring to its control block. If so, it must execute the deleter. In order for this to work, the control block must keep track of the pointer to pass to the deleter. It is not necessarily of the type T* or std::remove_extent_t<T>*.

The reason why these are not the same is that, for example, code like the following should work:

struct S {
    int member;
    int other_member;
    ~S();
};
void foo(std::shared_ptr<int>);
int main() {
    std::shared_ptr<S> sp = std::make_shared<S>();
    std::shared_ptr<int> ip(sp, &sp->member);
    foo(std::move(ip));
}

Here, sp owns an object of type S and also points to the same object. The function foo takes a std::shared_ptr<int> because it is part of some API that needs an int object that will remain alive for as long as the API isn't done with it (but the caller can also keep it alive for longer, if they want). The foo API doesn't care whether the int that you give it is part of some larger object; it just cares that the int will not be destroyed while it is holding on to it. So, we can create a std::shared_ptr<int> named ip, which points to sp->member, and pass that to foo. Now, this int object can only survive as long as the enclosing S object is alive. It follows that ip must, as long as it is alive, keep the entire S object alive. We could now call sp.reset() but the S object must remain alive, since there is still a shared_ptr referring to it. Finally, when ip is destroyed, it must destroy the entire S object, not just the int that it, itself, points to. Thus, it is not enough for the std::shared_ptr<int> instance ip to store a int* (which will be returned when .get() is called); the control block that it points to also has to store the S* to pass to the deleter.

For the same reason, your code will call the Foo destructor even though it is a std::shared_ptr<void> that is carrying out the destruction.

You asked: "Is there anything I am missing from the standard?" By this I assume you are asking whether the standard requires this behaviour and if so, where in the standard is it specified? The answer is yes. The standard specifies that a std::shared_ptr<T> stores a pointer and may also own a pointer; these two pointers need not be the same. In particular, [util.smartptr.shared.const]/14 describes constructors that "[construct] a shared_ptr instance that stores p and shares ownership with the initial value of r" (emphasis mine). The shared_ptr instance thus created may own a pointer that is different from the one it stores. However, when it is destroyed, [util.smartptr.shared.dest]/1 applies: if this is the last instance, the owned pointer is deleted (not the stored one).

CodePudding user response:

std::shared_ptr::shared_ptr - cppreference.com

constexpr shared_ptr() noexcept; (1)
constexpr shared_ptr( std::nullptr_t ) noexcept; (2)
template< class Y > explicit shared_ptr( Y* ptr ); (3)
..... ...

....

3-7) Constructs a shared_ptr with ptr as the pointer to the managed object.

For (3-4,6), Y* must be convertible to T*. (until C 17)
If T is an array type U[N], (3-4,6) do not participate in overload resolution if Y(*)[N] is not convertible to T*. If T is an array type U[], (3-4,6) do not participate in overload resolution if Y(*)[] is not convertible to T*. Otherwise, (3-4,6) do not participate in overload resolution if Y* is not convertible to T*. (since C 17)

Additionally:

  1. Uses the delete-expression delete ptr if T is not an array type; delete[] ptr if T is an array type (since C 17) as the deleter. Y must be a complete type. The delete expression must be well-formed, have well-defined behavior and not throw any exceptions. This constructor additionally does not participate in overload resolution if the delete expression is not well-formed. (since C 17)

So basically third form is used.

Also data holding reference counters (strong and weak) holds also information about destructor of the object. This (3) form of constructor fetches this information.

Note that std::unique_ptr by default do not hold such information and so it will fail in this scenario (fails to compile).

  • Related