Why can't a weak_ptr be constructed from a unique_ptr?

31,618

Solution 1

std::weak_ptr can't be used unless you convert it to std::shared_ptr by the means of lock(). if the standard allowed what you suggest, that means that you need to convert std::weak_ptr to unique in order to use it, violating the uniqueness (or re-inventing std::shared_ptr)

In order to illustrate, look at the two pieces of code:

std::shared_ptr<int> shared = std::make_shared<int>(10);
std::weak_ptr<int> weak(shared);

{
*(weak.lock()) = 20; //OK, the temporary shared_ptr will be destroyed but the pointee-integer still has shared  to keep it alive
}

Now with your suggestion:

std::unique_ptr<int> unique = std::make_unique<int>(10);
std::weak_ptr<int> weak(unique);

{
*(weak.lock()) = 20; //not OK. the temporary unique_ptr will be destroyed but unique still points at it! 
}

That has been said, you may suggest that there is only one unique_ptr, and you still can dereference weak_ptr (without creating another unique_ptr) then there is no problem. But then what is the difference between unique_ptr and shared_ptr with one reference? or moreover, what is the difference between a regular unique_ptr and C-pointers an get by using get?

weak_ptr is not for "general nonowning resources", it has a very specific job - The main goal of weak_ptr is to prevent circular pointing of shared_ptr which will make a memory leak. Anything else needs to be done with plain unique_ptr and shared_ptr.

Solution 2

If you think about it, a weak_ptr must refer to something other than the object itself. That's because the object can cease to exist (when there are no more strong pointers to it) and the weak_ptr still has to refer to something that contains the information that the object no longer exists.

With a shared_ptr, that something is the thing that contains the reference count. But with a unique_ptr, there is no reference count, so there is no thing that contains the reference count, thus nothing to continue to exist when the object is gone. So there's nothing for a weak_ptr to refer to.

There would also be no sane way to use such a weak_ptr. To use it, you'd have to have some way to guarantee that the object wasn't destroyed while you were using it. That's easy with a shared_ptr -- that's what a shared_ptr does. But how do you do that with a unique_ptr? You obviously can't have two of them, and something else must already own the object or it would have been destroyed since your pointer is weak.

Solution 3

A shared_ptr basically has two parts:

  1. the pointed-to object
  2. the reference count object

Once the reference count drops to zero the object (#1) is deleted.

The weak_ptr needs to be able to know if the object (#1) still exists. In order to do this, it has to be able to see the reference count object (#2), if it's not zero it can create a shared_ptr for the object (by incrementing the reference count). If the count is zero it will return an empty shared_ptr.

Consider the question of when can the reference count object (#2) be deleted? We must wait until no shared_ptr OR weak_ptr object refer to it. For this purpose the reference count object holds two reference counts, a strong ref and a weak ref. The reference count object will only be deleted when both these counts are zero. This means that part of the memory can only be freed after all the weak references are gone (this implies a hidden disadvantage with make_shared).

tl;dr; weak_ptr depends on a weak reference count which is part of shared_ptr, there cannot be a weak_ptr without a shared_ptr.

Solution 4

Conceptually, there is nothing preventing an implementation where a weak_ptr only provides access and a unique_ptr controls the lifetime. However, there are problems with that:

  • unique_ptr doesn't use reference counting to begin with. Adding the management structure for managing the weak references would be possible, but require an additional dynamic allocation. Since unique_ptr is supposed to avoid any(!) runtime overhead over a raw pointer, that overhead is not acceptable.
  • In order to use the object referenced by a weak_ptr, you need to extract a "real" reference from it, which will first validate that the pointer is not expired first and then give you this real reference (a shared_ptr in this case). This means that you suddenly have a second reference to an object that is supposed to be uniquely owned, which is a recipe for errors. This can't be fixed by returning a mixed half-strong pointer that only temporarily delays possible destruction of the pointee, because you could just as well store that one, too, defeating the idea behind unique_ptr.

Just wondering, what problem are you trying to solve using a weak_ptr here?

Solution 5

No-one has mentioned the performance aspect of the problem yet, so let me throw my $0.02 in.

weak_ptr must somehow know when the corresponding shared_ptrs have all gone out of scope and the pointed object has been deallocated and destroyed. This means that shared_ptrs need to communicate the destruction towards each weak_ptr to the same object somehow. This has a certain cost – for example, a global hash table needs to be updated, where weak_ptr gets the address from (or nullptr if the object is destroyed).

This also involves locking in a multi-threaded environment, so it can potentially be too slow for some tasks.

However, the goal of unique_ptr is to provide a zero-cost RAII-style abstraction class. Hence, it should not incur any other cost than that of deleteing (or delete[]ing) the dynamically allocated object. The delay imposed by doing a locked or otherwise guarded hash table access, for example, may be comparable to the cost of deallocation, however, which is not desirable in the case of unique_ptr.

Share:
31,618
notadam
Author by

notadam

stop stalking me

Updated on August 01, 2022

Comments

  • notadam
    notadam almost 2 years

    If I understand correctly, a weak_ptr doesn't increment the reference count of the managed object, therefore it doesn't represent ownership. It simply lets you access an object, the lifetime of which is managed by someone else. So I don't really see why a weak_ptr can't be constructed from a unique_ptr, but only a shared_ptr.

    Can someone briefly explain this?

  • David Schwartz
    David Schwartz over 9 years
    In effect, a weak_ptr is a shared_ptr to the reference count object.
  • notadam
    notadam over 9 years
    Nothing, this is all theoretical.
  • Peter - Reinstate Monica
    Peter - Reinstate Monica over 7 years
    And C++ trying to avoid overhead where none is necessary would not burden unique_ptrs with a dynamic allocation for a control block which can hold a common reference count (the way shared_ptrs are burdened).
  • user904963
    user904963 over 2 years
    unique_ptr has overhead. It's just minimal. Bjarne Stroustrup has said as much, and he's a proponent of safer, slightly less efficient code instead of code that's "too clever". He's supremely knowledgeable, and he's fairly good at directing a language in the direction it needs to go to remain relevant. The original direction was total speed with zero weight abstractions. Nowadays, with more and more lower-level code needed to code most things worthwhile and with paying programmers costing more than just buying more hardware, development time with fewer bugs is king even if less efficient.
  • Ulrich Eckhardt
    Ulrich Eckhardt over 2 years
    Do you have a citation for that? BTW: Quite some effort was put into the core language to guarantee correctness at compile time, which avoids runtime overhead. Move constructors are used in places where pointers were used in C98. Also, think about Rust, which takes this to another level.
  • user904963
    user904963 over 2 years
    @UlrichEckhardt Here is a great talk that discusses unique_ptr for quite a while. For starters, the simple code most people would write was enormous compared to a raw pointer. After a few smart uses of noexcept and move semantics, the code is still less efficient. Stroustrup knows what he's talking about. I won't be able to find the exact minute from the hours I've listened to him talk about C++ though. It's from a talk at CppCon in the last few years. It's a decent guess that the compiler will remove all overhead, but it isn't true.