Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is empty weak_ptr required to store null pointer, while empty shared_ptr is allowed to store non-null pointer?

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.

like image 528
Igor G Avatar asked Oct 18 '25 00:10

Igor G


1 Answers

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 locking 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.

like image 97
Nicol Bolas Avatar answered Oct 19 '25 15:10

Nicol Bolas