Naive thinking
I expected the following assertion (1) to hold true for every valid value of original argument:
#include <memory>
#include <cassert>
void foo(std::shared_ptr<int> original)
{
std::weak_ptr<int> weak{original};
std::shared_ptr<int> restored{weak.lock()}; // lock() explicitly to avoid exception
assert( restored == original ); // (1)
}
In other words, I thought that weak_ptr is supposed to be able to store the value of a shared_ptr in non-owning manner and then restore that original value later, when locked. Assuming the pointed object is still alive, of course.
Reality
Obviously I was wrong, as the following test turned out to fail the assertion:
void test()
{
int x = 42;
std::shared_ptr<int> empty_but_nonnull{std::shared_ptr<char>{}, &x};
foo(empty_but_nonnull);
}
Wording and test explained
shared_ptr can own one object, but store a pointer to some other object.
shared_ptr is called empty if it owns no object.
shared_ptr is called null if it points to no object.
A shared_ptr can be empty, but non-null. Such behavior is explicitly mentioned and allowed by the standard:
[util.smartptr.shared.const]:
17. [Note 2: This constructor allows creation of an empty shared_ptr instance with a non-null stored pointer. — end note]
Empty but non-null shared_ptr can be dereferenced, it is implicitly converted to true in conditional expressions, etc. No mention of the program being ill-formed whatsoever. So it seems to be perfectly legit and usable. It just says: "I'm pointing to an object that doesn't require any lifetime management, possibly to a global object with static storage duration, so what?".
However, an empty weak_ptr cannot be non-null. It is explicitly required by the standard to be null (emphasis mine):
[util.smartptr.weak.const]:
4.template<class Y> weak_ptr(const shared_ptr<Y>& r) noexcept;
Effects: If r is empty, constructs an empty weak_ptr object that stores a null pointer value
Which means that constructing a weak_ptr from an empty shared_ptr loses the pointer stored in original shared_ptr, forcing every empty weak_ptr to be null. After that, any attempt to reconstruct the original shared_ptr would obviously fail.
The Question
What's the rationale behind the emphasized part of the quoted clause on weak_ptr? Why shouldn't weak_ptr store the same pointer as the original shared_ptr?
It's just that my original naive thinking doesn't seem that illogical to me...
Edit: Example edited to eliminate any possible exception and undefined behavior.
shared_ptr<T>hold a pointer to aTand owns some object of typeUwhich may or may not be the same asT. This feature exist to allow theshared_ptr<T>to point to some object that is a component of theUobject that it owns. For example, you can create ashared_ptr<derived_class>and convert it to ashared_ptr<base_class>while still owning aderived_class. Or you can have ashared_ptr<some_struct>and create ashared_ptr<some_structs_member>which still owns asome_struct. Being able to do these things is the feature's purpose.You will note that in the above cases, the object being pointed to is owned by the object being owned. That is, if the owned
Uobject is destroyed, theTpointer held by theshared_ptris no longer valid.weak_ptr<T>is not a tool that is meant to be used to reconstitute ashared_ptr<T>. It is a tool that has one purpose: if ashared_ptrstill exists which owns the memory weakly owned by theweak_ptr<T>, then you may extract ashared_ptr<T>from it.weak_ptr<T>::lockdoes not care about theTpointer (it does preserve it, but it doesn't care about it); it only cares about ownership. Successfullylocking aweak_ptrmeans that the object being managed is still alive. And the only definition of "successfully locking" is whether theshared_ptr<T>isnullptr. With the exception ofuse_countthere is no observable difference between a non-nullshared_ptr<T>that owns aUand a non-nullshared_ptr<T>that doesn't own aU.Remember: the expectation is that the
Tbeing stored is an object that is owned byU. Therefore, if aweak_ptr<T>is going to claim weak ownership over ashared_ptr<T>, and it finds that theshared_ptr<T>does not own anything... it assumes that theTis also destroyed. Because that's what the feature is for.Now, could the constructor of
shared_ptr<T>from ashared_ptr<U>also check to see if theshared_ptr<U>it is given actually owns something and throw an exception in that case? It could, but C++ is not a safe language. Furthermore, the only exceptionsshared_ptr<T>constructors emit (other than implementation-defined ones) are from failure to allocate memory or failure to lock aweak_ptr<T>.So at the end of the day, the standard assumes you know what you're doing when creating the
shared_ptr.