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.
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);
}
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.
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 a T
and owns some object of type U
which may or may not be the same as T
. This feature exist to allow the shared_ptr<T>
to point to some object that is a component of the U
object that it owns. For example, you can create a shared_ptr<derived_class>
and convert it to a shared_ptr<base_class>
while still owning a derived_class
. Or you can have a shared_ptr<some_struct>
and create a shared_ptr<some_structs_member>
which still owns a some_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 U
object is destroyed, the T
pointer held by the shared_ptr
is no longer valid.
weak_ptr<T>
is not a tool that is meant to be used to reconstitute a shared_ptr<T>
. It is a tool that has one purpose: if a shared_ptr
still exists which owns the memory weakly owned by the weak_ptr<T>
, then you may extract a shared_ptr<T>
from it.
weak_ptr<T>::lock
does not care about the T
pointer (it does preserve it, but it doesn't care about it); it only cares about ownership. Successfully lock
ing a weak_ptr
means that the object being managed is still alive. And the only definition of "successfully locking" is whether the shared_ptr<T>
is nullptr
. With the exception of use_count
there is no observable difference between a non-null shared_ptr<T>
that owns a U
and a non-null shared_ptr<T>
that doesn't own a U
.
Remember: the expectation is that the T
being stored is an object that is owned by U
. Therefore, if a weak_ptr<T>
is going to claim weak ownership over a shared_ptr<T>
, and it finds that the shared_ptr<T>
does not own anything... it assumes that the T
is also destroyed. Because that's what the feature is for.
Now, could the constructor of shared_ptr<T>
from a shared_ptr<U>
also check to see if the shared_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 exceptions shared_ptr<T>
constructors emit (other than implementation-defined ones) are from failure to allocate memory or failure to lock a weak_ptr<T>
.
So at the end of the day, the standard assumes you know what you're doing when creating the shared_ptr
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With