How does a weak_ptr know that the shared resources

2019-02-12 08:28发布

问题:

Considering the following code:

#include <memory>
#include <iostream>

using namespace std;

struct MySharedStruct
{
  int i;
};

void print_value_of_i(weak_ptr<MySharedStruct> weakPtr)
{
  if (shared_ptr<MySharedStruct> sp = weakPtr.lock())
  { cout << "Value of i = " << sp->i << endl; }
  else
  { cout << "Resource has expired"; }
}

int main()
{
  shared_ptr<MySharedStruct> sharedPtr(new MySharedStruct() );
  sharedPtr->i = 5;

  weak_ptr<MySharedStruct> weakPtr;
  weakPtr = sharedPtr;

  print_value_of_i(weakPtr);

  sharedPtr.reset(new MySharedStruct() ); // <<----- How does weak_ptr know it has expired after this line executes?
  sharedPtr->i = 10;

  print_value_of_i(weakPtr);

  return 0;
}

How does the weak_ptr know it has expired considering the resource that shared_ptr was referencing has been essentially replaced by another resource? What does weak_ptr keep track of to know for sure that the old shared resource was destroyed and replaced by the new shared resource? Example definitions (if relevant) of methods such lock in weak_ptr would be appreciated.

回答1:

The control block allocated when a shared_ptr is created from a plain pointer contains both the reference counter for the object and the pointer to the object itself and the custom deleter object if any. When that reference counter reaches zero the object is released and the pointer is set to null. So, when the object reference counter is zero it means that the object is gone.

For x86 and x86-64 they use atomic operations and no explicit locking (no mutex or spinlock). The trick of the implementation is a special lock-free (code language for busy spin) function atomic_conditional_increment that only increments the object reference counter if it is not zero. It is used in the implementation of weak_ptr::lock function to cope with a race when more than one thread tries to create a shared_ptr from the same weak_ptr with object reference counter being zero. See http://www.boost.org/doc/libs/1_52_0/boost/smart_ptr/detail/sp_counted_base_gcc_x86.hpp

The control block itself is shared between shared_ptr's and weak_ptr's and has another reference counter for itself, so that it stays alive till the last reference to it is released.

When a shared_ptr is reassigned it points to another control block, so that a control block only ever points to one same object. In other words, there is no replacement of one object with another in the control block.



回答2:

Short answer

I suspect most implementations achieve this by having a shared control block that both weakPtr and sharedPtr refer to. When sharedPtr is reset, it decrements a use_count in the control block that the weakPtr can use to test if the pointer is valid.

Long answer

But I think that could vary depending on the implementation. Here's a blow-by-blow of what the C++11 standard says should happen:

shared_ptr<MySharedStruct> sharedPtr(new MySharedStruct());

Per 20.7.2.2.1, sharedPtr is constructed with ownership of the given data.

weak_ptr<MySharedStruct> weakPtr;
weakPtr = sharedPtr;

Per 20.7.2.3.1, weakPtr is constructed and then assigned a value of sharedPtr. After the assignment, weakPtr and sharedPtr now share ownership of the given data.

sharedPtr.reset(new MySharedStruct());

Per 20.7.2.2.4, reset(Y*) is equivalent to shared_ptr(Y*).swap(*this). In other words, sharedPtr swaps its contents with a temporary shared_ptr that owns the new data.

After the swap, sharedPtr will own the new data, and the temporary will share ownership of the old data with weakPtr.

Per 20.7.2.2.2, the temporary is then destructed:

  • Since the temporary owns the old data and does not share that ownership with another shared_ptr instance, it deletes the old data.
  • All instances that share ownership with the temporary shared_ptr will report a use_count() that is one less than its previous value.

That means that weakPtr.use_count() == 0.

if (shared_ptr<MySharedStruct> sp = weakPtr.lock()) { 
  cout << "Value of i = " << sp->i << endl;
} else {
  cout << "Resource has expired"; 
}

Per 20.7.2.3.5, calling lock is equivalent to

expired() ? shared_ptr<T>() : shared_ptr<T>(*this)

...and expired() is equivalent to

use_count() == 0

...which means lock will return an empty shared_ptr.