John
John

Reputation: 3484

Why does "weak.lock()" return "nullptr" with the definition of "auto weak=std::make_shared<int>(42);"?

Why does weak.lock() return nullptr in this code snippest:

   std::weak_ptr<int> weakPtr1 = std::make_shared<int>(6);
   std::cout << weakPtr1.lock() << std::endl;

whereas it works in the following one:

   std::shared_ptr<int> sharedPtr = std::make_shared<int>(99);
   std::weak_ptr<int> weakPtr2 = sharedPtr;
   std::cout << weakPtr2.lock() << std::endl;

Check cpp.sh/9gkys.

I have thought and thought about it, but I am still confused now. I would be grateful to have some help with this question.

Upvotes: 5

Views: 482

Answers (2)

Piotr Skotnicki
Piotr Skotnicki

Reputation: 48447

Smart pointers, in order to do their work properly, maintain a so called control block which serves as a metadata storage, in particular, use counters. That is, each resource has an associated control block in memory (consisting of e.g. two integers) that smart pointers can refer to to know how many of them are still using/observing the resource. Obviously, each existing std::shared_ptr increases the use counter stored in the control block, so that its destructor knows whether or not it's time to release the resource on destruction. std::weak_ptr, in turn, only tracks the object and its control block. Note that here's an important detail: std::weak_ptr does not increase the use counter. That's desirable, as its main purpose is to break possible cycles between a pair of objects observing one another. That is, if two objects would store std::shared_ptrs one to another, then such a pair of objects would also keep alive one another endlessly.

How can a std::weak_ptr know if the resource can be lock()ed ? This can succeed only if the use counter is greater than zero. It knows that from the control block (which itself remains alive in memory as long as there's also non-zero weak pointers observing it).

In the first example:

std::weak_ptr<int> weakPtr1 = std::make_shared<int>(6);

both a resource (int=6) is allocated and also its control block. Use counter becomes 1, and remains so as long as std::shared_ptr is alive. Then, a std::weak_ptr is initialized, obtaining a pointer to the control block. Here, it won't increase the use counter. It will, however, increase the counter of weak pointers. At this point, both counters are 1. Then, at the semicolon ;, the temporary std::shared_ptr is destroyed. It decreases the use counter down to 0. This means there are no more shared pointers sharing ownership of the resource, which allows that resource to be released. However, there is still 1 weak pointer observing the control block, which means the control block itself will remain in the memory, so that weakPtr1 knows it won't be able to lock() the resource anymore (because that resource no longer exists).

In the second example:

std::shared_ptr<int> sharedPtr = std::make_shared<int>(99);
std::weak_ptr<int> weakPtr2 = sharedPtr;

both the resource int=99 and its control block remain alive. Hence weakPtr2 can be locked as long as sharedPtr (or any of its copies) is not destroyed.

Upvotes: 4

kabanus
kabanus

Reputation: 25895

Your examples use copy initialization. As such, the shared_ptr constructed on the right lives only until the evaluation of the expression, and is then destroyed:

  1. In the first example this means there are no more references to the shared_ptr (we do not count the weak one), and hence lock returns null.
  2. In the second one you bind the result to a local variable, extending life time to the current block - and hence there is still a reference, and no null result.

Upvotes: 3

Related Questions